diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 4371fbe7..9e23bdd0 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -4,13 +4,25 @@
+
+
+
+
diff --git a/README.md b/README.md
index c8950aad..c2b68f30 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ from scratch.
### Orbit ❤️ Android
-- Subscribe to state and side effects through LiveData
+- Subscribe to state and side effects through Flow
- ViewModel support, along with SavedState!
### Testing 🤖
@@ -159,9 +159,13 @@ projects as well as lifecycle independent services.
## Connecting to a ViewModel
-Now we need to wire up the `ViewModel` to our UI. Orbit provides various methods
-of connecting via optional modules. For Android, the most convenient way to
-connect is via `LiveData`, as it manages subscription disposal automatically.
+Now we need to wire up the `ViewModel` to our UI. We expose coroutine
+`Flow`s through which one can conveniently subscribe to updates.
+Alternatively you can convert these to your preferred type using
+externally provided extension methods e.g.
+[asLiveData](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#(kotlinx.coroutines.flow.Flow).asLiveData(kotlin.coroutines.CoroutineContext,%20kotlin.Long))
+or
+[asObservable](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-rx3/kotlinx.coroutines.rx3/kotlinx.coroutines.flow.-flow/as-observable.html).
``` kotlin
class CalculatorActivity: AppCompatActivity() {
@@ -174,10 +178,12 @@ class CalculatorActivity: AppCompatActivity() {
addButton.setOnClickListener { viewModel.add(1234) }
subtractButton.setOnClickListener { viewModel.subtract(1234) }
- // NOTE: Live data support is provided by the live data module:
- // com.babylon.orbit2:orbit-livedata
- viewModel.container.state.observe(this, Observer { render(it) })
- viewModel.container.sideEffect.observe(this, Observer { handleSideEffect(it) })
+ lifecycleScope.launchWhenCreated {
+ viewModel.container.stateFlow.collect { render(it) }
+ }
+ lifecycleScope.launchWhenCreated {
+ viewModel.container.sideEffectFlow.collect { handleSideEffect(it) }
+ }
}
private fun render(state: CalculatorState) {
diff --git a/orbit-2-core/README.md b/orbit-2-core/README.md
index 8c7deb1e..8fb4cd47 100644
--- a/orbit-2-core/README.md
+++ b/orbit-2-core/README.md
@@ -7,8 +7,10 @@ It provides all the basic parts of Orbit.
- [Architecture](#architecture)
- [Orbit concepts](#orbit-concepts)
- [Side effects](#side-effects)
+ - [Limitations](#limitations)
- [Including the module](#including-the-module)
- [Orbit container](#orbit-container)
+ - [Subscribing to the container](#subscribing-to-the-container)
- [ContainerHost](#containerhost)
- [Core Orbit operators](#core-orbit-operators)
- [Transform](#transform)
@@ -76,6 +78,17 @@ The UI does not have to be aware of all side effects (e.g. why should the UI
care if you send analytics events?). As such you can have side effects that do
not post any event back to the UI.
+Side effects are cached if there are no observers, guaranteeing critical
+events such as navigation are delivered after re-subscription.
+
+#### Limitations
+
+`Container.sideEffectFlow` is designed to be collected by only one
+observer. This ensures that side effect caching works in a predictable
+way. If your particular use case requires multi-casting use `broadcast`
+on the side effect flow, but be aware that caching will not work for the
+resulting `BroadcastChannel`.
+
## Including the module
Orbit 2 is a modular framework. You will need this module to get started!
@@ -93,6 +106,43 @@ Orbit MVI system. It retains the state, allows you to listen to side effects and
state updates and allows you to modify the state through the `orbit` function
which executes Orbit operators of your desired business logic.
+### Subscribing to the container
+
+[Container](src/main/java/com/babylon/orbit2/Container.kt) exposes flows
+that emit updates to the container state and side effects.
+
+- State emissions are conflated
+- Side effects are cached by default if no observers are listening. This
+ can be changed via
+ [Container Settings](src/main/java/com/babylon/orbit2/Container.kt#Settings)
+
+``` kotlin
+data class ExampleState(val seen: List = emptyList())
+
+sealed class ExampleSideEffect {
+ data class Toast(val text: String)
+}
+
+fun main() {
+ // create a container
+ val container = container(ExampleState())
+
+ // subscribe to updates
+ // For Android, use `lifecycleScope.launchWhenCreated` instead
+ CoroutineScope(Dispatchers.Main).launch {
+ container.stateFlow.collect {
+ // do something with the state
+ }
+ }
+ CoroutineScope(Dispatchers.Main).launch {
+ container.sideEffectFlow.collect {
+ // do something with the side effect
+ }
+ }
+}
+
+```
+
### ContainerHost
A [ContainerHost](src/main/java/com/babylon/orbit2/ContainerHost.kt) is not
@@ -122,16 +172,10 @@ Operators are invoked via the `orbit` function in a
commonly, a [Container](src/main/java/com/babylon/orbit2/Container.kt) directly)
For more information about which threads these operators run on please see
-[threading](#threading).
+[Threading](#threading).
``` kotlin
-data class ExampleState(val seen: List = emptyList())
-
-sealed class ExampleSideEffect {
- data class Toast(val text: String)
-}
-
-class ExampleViewModel : ContainerHost, ViewModel() {
+class Example : ContainerHost {
override val container = container(ExampleState())
fun example(number: Int) = orbit {
@@ -152,7 +196,7 @@ easily.
### Transform
``` kotlin
-class ExampleViewModel : ContainerHost {
+class Example : ContainerHost {
...
fun example(number: Int) = orbit {
@@ -177,7 +221,7 @@ a backend API or subscribe to a stream of location updates.
### Reduce
``` kotlin
-class ExampleViewModel : ContainerHost {
+class Example : ContainerHost {
...
fun example(number: Int) = orbit {
@@ -204,7 +248,7 @@ upstream reduction has completed.
### Side effect
``` kotlin
-class ExampleViewModel : ContainerHost {
+class Example : ContainerHost {
...
fun example(number: Int) = orbit {
@@ -253,7 +297,7 @@ Examples of using the exposed fields:
``` kotlin
perform("Toast the current state")
-class ExampleViewModel : ContainerHost {
+class Example : ContainerHost {
...
fun anotherExample(number: Int) = orbit {
@@ -268,7 +312,7 @@ class ExampleViewModel : ContainerHost {
``` kotlin
perform("Toast the current state")
-class ExampleViewModel : ContainerHost, ViewModel() {
+class Example : ContainerHost {
override val container = container(ExampleState()) {
onCreate()
}
@@ -302,15 +346,10 @@ done within particular `transform` blocks e.g. `transformSuspend`.
- `transform` and `transformX` calls execute in an `IO` thread so as not to
block the Orbit [Container](src/main/java/com/babylon/orbit2/Container.kt)
from accepting further events.
-- Updates delivered via `Container.stateStream` and `Container.sideEffectStream`
- come in on the same thread you call `Stream.connect` on. However the
- connection to the stream has to be manually managed and cancelled. To make it
- more convenient to consume a `Stream` we have created wrappers to turn it into
- e.g. `LiveData`.
## Error handling
-It is good practice to handle all of your errors within your flows. Orbit
-does not provide any built-in exception handling because it cannot make
-assumptions about how you respond to errors, avoiding putting your system in an
-undefined state.
+It is good practice to handle all of your errors within your flows.
+Orbit does not provide any built-in exception handling because it cannot
+make assumptions about how you respond to errors, avoiding putting your
+system in an undefined state.
diff --git a/orbit-2-core/orbit-2-core_build.gradle.kts b/orbit-2-core/orbit-2-core_build.gradle.kts
index b2abb748..697ed794 100644
--- a/orbit-2-core/orbit-2-core_build.gradle.kts
+++ b/orbit-2-core/orbit-2-core_build.gradle.kts
@@ -27,6 +27,7 @@ dependencies {
// Testing
testImplementation(project(":orbit-2-test"))
+ testImplementation(ProjectDependencies.kotlinCoroutinesTest)
GroupedDependencies.testsImplementation.forEach { testImplementation(it) }
testRuntimeOnly(ProjectDependencies.junitJupiterEngine)
}
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/BaseDslPlugin.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/BaseDslPlugin.kt
index c2189420..c8152dc2 100644
--- a/orbit-2-core/src/main/java/com/babylon/orbit2/BaseDslPlugin.kt
+++ b/orbit-2-core/src/main/java/com/babylon/orbit2/BaseDslPlugin.kt
@@ -123,7 +123,7 @@ object BaseDslPlugin : OrbitDslPlugin {
}
is Reduce -> flow.onEach { event ->
containerContext.withIdling(operator) {
- containerContext.setState.send(
+ containerContext.setState(
createContext(event).block() as S
)
}
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/Container.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/Container.kt
index 5f084631..d6ecc967 100644
--- a/orbit-2-core/src/main/java/com/babylon/orbit2/Container.kt
+++ b/orbit-2-core/src/main/java/com/babylon/orbit2/Container.kt
@@ -18,6 +18,8 @@ package com.babylon.orbit2
import com.babylon.orbit2.idling.IdlingResource
import com.babylon.orbit2.idling.NoopIdlingResource
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
/**
* The heart of the Orbit MVI system. Represents an MVI container with its input and outputs.
@@ -33,17 +35,43 @@ interface Container {
*/
val currentState: STATE
+ /**
+ * A [Flow] of state updates. Emits the latest state upon subscription and serves only distinct
+ * values (through equality comparison).
+ */
+ val stateFlow: Flow
+
+ /**
+ * A [Flow] of one-off side effects posted from [Builder.sideEffect]. Caches side effects when there are no collectors.
+ * The size of the cache can be controlled via Container [Settings] and determines if and when the orbit thread suspends when you
+ * post a side effect. The default is unlimited. You don't have to touch this unless you are posting many side effects which could result in
+ * [OutOfMemoryError].
+ *
+ * This is designed to be collected by one observer only in order to ensure that side effect caching works in a predictable way.
+ * If your particular use case requires multi-casting use `broadcast` on this [Flow], but be aware that caching will not work for the
+ * resulting `BroadcastChannel`.
+ */
+ val sideEffectFlow: Flow
+
/**
* A [Stream] of state updates. Emits the latest state upon subscription and serves only distinct
* values (only changed states are emitted) by default.
+ * Emissions come in on the main coroutine dispatcher if installed, with the default dispatcher as the fallback. However,
+ * the connection to the stream has to be manually managed and cancelled when appropriate.
*/
+ @Suppress("DEPRECATION")
+ @Deprecated("stateStream is deprecated and will be removed in Orbit 1.2.0, use stateFlow instead")
val stateStream: Stream
/**
* A [Stream] of one-off side effects posted from [Builder.sideEffect].
* Depending on the [Settings] this container has been instantiated with, can support
* side effect caching when there are no listeners (default).
+ * Emissions come in on the main coroutine dispatcher if installed, with the default dispatcher as the fallback. However,
+ * the connection to the stream has to be manually managed and cancelled when appropriate.
*/
+ @Suppress("DEPRECATION")
+ @Deprecated("sideEffectStream is deprecated and will be removed in Orbit 1.2.0, use sideEffectFlow instead")
val sideEffectStream: Stream
/**
@@ -59,12 +87,13 @@ interface Container {
/**
* Represents additional settings to create the container with.
*
- * @property sideEffectCaching When true the side effects are cached when there are no
- * subscribers, to be emitted later upon first subscription.
- * On by default.
+ * @property sideEffectBufferSize Defines how many side effects can be buffered before the container suspends. If you are
+ * sending many side effects and getting out of memory exceptions this can be turned down to suspend the container instead.
+ * Unlimited by default.
+ * @property idlingRegistry The registry used by the container for signalling idling for UI tests
*/
class Settings(
- val sideEffectCaching: Boolean = true,
+ val sideEffectBufferSize: Int = Channel.UNLIMITED,
val idlingRegistry: IdlingResource = NoopIdlingResource()
)
}
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/FlowExtensions.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/FlowExtensions.kt
new file mode 100644
index 00000000..8a6b7e6b
--- /dev/null
+++ b/orbit-2-core/src/main/java/com/babylon/orbit2/FlowExtensions.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 Babylon Partners Limited
+ *
+ * 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.babylon.orbit2
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import java.io.Closeable
+import kotlin.coroutines.EmptyCoroutineContext
+
+@ExperimentalCoroutinesApi
+@Suppress("DEPRECATION")
+internal fun Flow.asStream(): Stream {
+ return object : Stream {
+ override fun observe(lambda: (T) -> Unit): Closeable {
+
+ val job = CoroutineScope(streamCollectionDispatcher).launch {
+ this@asStream.collect {
+ lambda(it)
+ }
+ }
+
+ return Closeable { job.cancel() }
+ }
+ }
+}
+
+private val streamCollectionDispatcher
+ get() = try {
+ Dispatchers.Main.also {
+ it.isDispatchNeeded(EmptyCoroutineContext) // Try to perform an operation on the dispatcher
+ }
+ } catch (ias: IllegalStateException) {
+ Dispatchers.Default
+ }
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/LazyCreateContainerDecorator.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/LazyCreateContainerDecorator.kt
index 2f652d6e..22a8904d 100644
--- a/orbit-2-core/src/main/java/com/babylon/orbit2/LazyCreateContainerDecorator.kt
+++ b/orbit-2-core/src/main/java/com/babylon/orbit2/LazyCreateContainerDecorator.kt
@@ -16,9 +16,13 @@
package com.babylon.orbit2
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
import java.io.Closeable
import java.util.concurrent.atomic.AtomicBoolean
+@Suppress("OverridingDeprecatedMember", "DEPRECATION")
class LazyCreateContainerDecorator(
override val actual: Container,
val onCreate: (state: STATE) -> Unit
@@ -28,6 +32,18 @@ class LazyCreateContainerDecorator(
override val currentState: STATE
get() = actual.currentState
+ override val stateFlow: Flow
+ get() = flow {
+ runOnCreate()
+ emitAll(actual.stateFlow)
+ }
+
+ override val sideEffectFlow: Flow
+ get() = flow {
+ runOnCreate()
+ emitAll(actual.sideEffectFlow)
+ }
+
override val stateStream: Stream
get() = object : Stream {
override fun observe(lambda: (STATE) -> Unit): Closeable {
@@ -35,6 +51,7 @@ class LazyCreateContainerDecorator(
return actual.stateStream.observe(lambda)
}
}
+
override val sideEffectStream: Stream
get() = object : Stream {
override fun observe(lambda: (SIDE_EFFECT) -> Unit): Closeable {
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/OrbitDslPlugin.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/OrbitDslPlugin.kt
index cee51ad0..59322f6a 100644
--- a/orbit-2-core/src/main/java/com/babylon/orbit2/OrbitDslPlugin.kt
+++ b/orbit-2-core/src/main/java/com/babylon/orbit2/OrbitDslPlugin.kt
@@ -17,7 +17,6 @@
package com.babylon.orbit2
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
/**
@@ -33,7 +32,7 @@ interface OrbitDslPlugin {
class ContainerContext(
val backgroundDispatcher: CoroutineDispatcher,
- val setState: SendChannel,
+ val setState: (S) -> Unit,
val postSideEffect: (SE) -> Unit,
val settings: Container.Settings
)
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/RealContainer.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/RealContainer.kt
index 6ba7246e..1ea6278d 100644
--- a/orbit-2-core/src/main/java/com/babylon/orbit2/RealContainer.kt
+++ b/orbit-2-core/src/main/java/com/babylon/orbit2/RealContainer.kt
@@ -20,12 +20,13 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.plus
@@ -41,12 +42,12 @@ open class RealContainer(
parentScope: CoroutineScope
) : Container {
private val scope = parentScope + orbitDispatcher
- private val stateChannel = ConflatedBroadcastChannel(initialState)
- private val sideEffectChannel = Channel(Channel.RENDEZVOUS)
+ private val internalStateFlow = MutableStateFlow(initialState)
+ private val sideEffectChannel = Channel(settings.sideEffectBufferSize)
private val sideEffectMutex = Mutex()
- private val pluginContext = OrbitDslPlugin.ContainerContext(
+ private val pluginContext = OrbitDslPlugin.ContainerContext(
backgroundDispatcher = backgroundDispatcher,
- setState = stateChannel,
+ setState = { internalStateFlow.value = it },
postSideEffect = { event: SIDE_EFFECT ->
scope.launch {
// Ensure side effect ordering
@@ -67,16 +68,15 @@ open class RealContainer(
}
override val currentState: STATE
- get() = stateChannel.value
+ get() = internalStateFlow.value
- override val stateStream = stateChannel.asStateStream { currentState }
+ override val stateFlow = internalStateFlow
- override val sideEffectStream: Stream =
- if (settings.sideEffectCaching) {
- sideEffectChannel.asCachingStream(scope)
- } else {
- sideEffectChannel.asNonCachingStream()
- }
+ override val sideEffectFlow: Flow get() = sideEffectChannel.receiveAsFlow()
+
+ override val stateStream = stateFlow.asStream()
+
+ override val sideEffectStream = sideEffectFlow.asStream()
override fun orbit(init: Builder.() -> Builder) {
scope.launch {
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/Stream.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/Stream.kt
index 68113047..855e5779 100644
--- a/orbit-2-core/src/main/java/com/babylon/orbit2/Stream.kt
+++ b/orbit-2-core/src/main/java/com/babylon/orbit2/Stream.kt
@@ -18,15 +18,17 @@ package com.babylon.orbit2
import androidx.annotation.CheckResult
import java.io.Closeable
+import kotlinx.coroutines.MainCoroutineDispatcher
/**
* Represents a stream of values.
*
- * Observing happens on the thread where [observe] is called.
+ * Observing happens on [MainCoroutineDispatcher] if one is installed, otherwise on the default dispatcher.
*
* The subscription can be closed using the returned [Closeable]. It is the user's responsibility
* to manage the lifecycle of the subscription.
*/
+@Deprecated("Stream is deprecated and will be removed in Orbit 1.2.0, use stateFlow instead")
interface Stream {
@CheckResult
diff --git a/orbit-2-core/src/main/java/com/babylon/orbit2/StreamExtensions.kt b/orbit-2-core/src/main/java/com/babylon/orbit2/StreamExtensions.kt
deleted file mode 100644
index 56a8b9fc..00000000
--- a/orbit-2-core/src/main/java/com/babylon/orbit2/StreamExtensions.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 2020 Babylon Partners Limited
- *
- * 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.babylon.orbit2
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.BroadcastChannel
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.broadcast
-import kotlinx.coroutines.launch
-import java.io.Closeable
-import java.util.concurrent.atomic.AtomicInteger
-
-@ExperimentalCoroutinesApi
-internal fun BroadcastChannel.asStateStream(initial: () -> T): Stream {
- return object : Stream {
- override fun observe(lambda: (T) -> Unit): Closeable {
- val sub = this@asStateStream.openSubscription()
-
- CoroutineScope(Dispatchers.Unconfined).launch {
- var lastState = initial()
- lambda(lastState)
-
- for (state in sub) {
- if (state != lastState) {
- lastState = state
- lambda(state)
- }
- }
- }
- return Closeable { sub.cancel() }
- }
- }
-}
-
-@ExperimentalCoroutinesApi
-internal fun Channel.asNonCachingStream(): Stream {
- val broadcastChannel = this.broadcast(start = CoroutineStart.DEFAULT)
-
- return object : Stream {
- override fun observe(lambda: (T) -> Unit): Closeable {
- val receiveChannel = broadcastChannel.openSubscription()
- CoroutineScope(Dispatchers.Unconfined).launch {
- for (item in receiveChannel) {
- lambda(item)
- }
- }
- return Closeable {
- receiveChannel.cancel()
- }
- }
- }
-}
-
-@ExperimentalCoroutinesApi
-internal fun Channel.asCachingStream(originalScope: CoroutineScope): Stream {
- return object : Stream {
- private val subCount = AtomicInteger(0)
- private val buffer = mutableListOf()
- private val channel = BroadcastChannel(Channel.BUFFERED)
-
- init {
- originalScope.launch {
- for (item in this@asCachingStream) {
- if (subCount.get() == 0) {
- buffer.add(item)
- } else {
- channel.send(item)
- }
- }
- }
- }
-
- override fun observe(lambda: (T) -> Unit): Closeable {
- val receiveChannel = channel.openSubscription()
-
- CoroutineScope(Dispatchers.Unconfined).launch {
- if (subCount.compareAndSet(0, 1)) {
- buffer.forEach { buffered ->
- channel.send(buffered)
- }
- buffer.clear()
- }
- for (item in receiveChannel) {
- lambda(item)
- }
- }
- return Closeable {
- receiveChannel.cancel()
- subCount.decrementAndGet()
- }
- }
- }
-}
diff --git a/orbit-2-core/src/test/java/com/babylon/orbit2/BaseDslPluginThreadingTest.kt b/orbit-2-core/src/test/java/com/babylon/orbit2/BaseDslPluginThreadingTest.kt
index 330fe7e5..0f6a70d4 100644
--- a/orbit-2-core/src/test/java/com/babylon/orbit2/BaseDslPluginThreadingTest.kt
+++ b/orbit-2-core/src/test/java/com/babylon/orbit2/BaseDslPluginThreadingTest.kt
@@ -37,11 +37,11 @@ internal class BaseDslPluginThreadingTest {
fun `reducer executes on orbit dispatcher`() {
val action = fixture()
val middleware = BaseDslMiddleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testFlowObserver = middleware.container.stateFlow.test()
middleware.reducer(action)
- testStreamObserver.awaitCount(2)
+ testFlowObserver.awaitCount(2)
assertThat(middleware.threadName).startsWith(ORBIT_THREAD_PREFIX)
}
@@ -49,11 +49,11 @@ internal class BaseDslPluginThreadingTest {
fun `transformer executes on background dispatcher`() {
val action = fixture()
val middleware = BaseDslMiddleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testFlowObserver = middleware.container.stateFlow.test()
middleware.transformer(action)
- testStreamObserver.awaitCount(2)
+ testFlowObserver.awaitCount(2)
assertThat(middleware.threadName).startsWith(BACKGROUND_THREAD_PREFIX)
}
@@ -61,11 +61,11 @@ internal class BaseDslPluginThreadingTest {
fun `posting side effects executes on orbit dispatcher`() {
val action = fixture()
val middleware = BaseDslMiddleware()
- val testStreamObserver = middleware.container.sideEffectStream.test()
+ val testFlowObserver = middleware.container.sideEffectFlow.test()
middleware.postingSideEffect(action)
- testStreamObserver.awaitCount(1)
+ testFlowObserver.awaitCount(1)
assertThat(middleware.threadName).startsWith(ORBIT_THREAD_PREFIX)
}
diff --git a/orbit-2-core/src/test/java/com/babylon/orbit2/BenchmarkTest.kt b/orbit-2-core/src/test/java/com/babylon/orbit2/BenchmarkTest.kt
index 6bbfabd1..c73fab28 100644
--- a/orbit-2-core/src/test/java/com/babylon/orbit2/BenchmarkTest.kt
+++ b/orbit-2-core/src/test/java/com/babylon/orbit2/BenchmarkTest.kt
@@ -16,36 +16,39 @@
package com.babylon.orbit2
+import com.appmattus.kotlinfixture.kotlinFixture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.junit.jupiter.api.Test
-import kotlin.random.Random
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis
internal class BenchmarkTest {
+ private val fixture = kotlinFixture()
+
@Test
fun benchmark() {
- val middleware = BenchmarkMiddleware()
- val testStreamObserver = middleware.container.stateStream.test()
val x = 100_000
+ val middleware = BenchmarkMiddleware(x)
+ val testStreamObserver = middleware.container.stateFlow.test()
- val actions = List(x) {
- Random.nextInt()
- }
+ val actions = fixture.asSequence().distinct().take(100_000)
- GlobalScope.launch {
- actions.forEach {
- middleware.reducer(it)
+ GlobalScope.launch {
+ actions.forEach {
+ middleware.reducer(it)
+ }
}
- }
val millisReducing = measureTimeMillis {
- testStreamObserver.awaitCount(x)
+ middleware.latch.await(10, TimeUnit.SECONDS)
}
+ println(testStreamObserver.values.size)
println(millisReducing)
val reduction: Float = millisReducing.toFloat() / x
println(reduction)
@@ -53,13 +56,15 @@ internal class BenchmarkTest {
private data class TestState(val id: Int)
- private class BenchmarkMiddleware : ContainerHost {
+ private class BenchmarkMiddleware(count: Int) : ContainerHost {
override val container =
CoroutineScope(Dispatchers.Unconfined).container(TestState(42))
+ val latch = CountDownLatch(count)
+
fun reducer(action: Int) = orbit {
reduce {
- state.copy(id = action)
+ state.copy(id = action).also { latch.countDown() }
}
}
}
diff --git a/orbit-2-core/src/test/java/com/babylon/orbit2/ContainerLifecycleTest.kt b/orbit-2-core/src/test/java/com/babylon/orbit2/ContainerLifecycleTest.kt
index 0a4c2c56..802eecb2 100644
--- a/orbit-2-core/src/test/java/com/babylon/orbit2/ContainerLifecycleTest.kt
+++ b/orbit-2-core/src/test/java/com/babylon/orbit2/ContainerLifecycleTest.kt
@@ -30,8 +30,8 @@ internal class ContainerLifecycleTest {
fun `onCreate is called once after connecting to the container`() {
val initialState = fixture()
val middleware = Middleware(initialState)
- val testStateObserver = middleware.container.stateStream.test()
- val testSideEffectObserver = middleware.container.sideEffectStream.test()
+ val testStateObserver = middleware.container.stateFlow.test()
+ val testSideEffectObserver = middleware.container.sideEffectFlow.test()
testStateObserver.awaitCount(1)
testSideEffectObserver.awaitCount(1)
diff --git a/orbit-2-core/src/test/java/com/babylon/orbit2/FlowToStreamThreadingTest.kt b/orbit-2-core/src/test/java/com/babylon/orbit2/FlowToStreamThreadingTest.kt
new file mode 100644
index 00000000..0242d2a4
--- /dev/null
+++ b/orbit-2-core/src/test/java/com/babylon/orbit2/FlowToStreamThreadingTest.kt
@@ -0,0 +1,78 @@
+package com.babylon.orbit2
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.ObsoleteCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.sendBlocking
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.newSingleThreadContext
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@ExperimentalCoroutinesApi
+internal class FlowToStreamThreadingTest {
+
+ @Nested
+ inner class Default {
+ @Test
+ fun `stream observed on default dispatcher if no main is installed`() {
+ val channel = Channel()
+ val latch = CountDownLatch(1)
+ var threadName = ""
+
+ channel.receiveAsFlow().asStream().observe {
+ threadName = Thread.currentThread().name
+ latch.countDown()
+ }
+
+ channel.sendBlocking(123)
+
+ latch.await(5, TimeUnit.SECONDS)
+
+ assertThat(threadName).startsWith("Default")
+ }
+ }
+
+ @ObsoleteCoroutinesApi
+ @Nested
+ inner class Main {
+
+ @BeforeEach
+ fun beforeEach() {
+ Dispatchers.setMain(
+ newSingleThreadContext("main")
+ )
+ }
+
+ @AfterEach
+ fun afterEach() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `stream observed on main dispatcher if installed`() {
+ val channel = Channel()
+ val latch = CountDownLatch(1)
+ var threadName = ""
+
+ channel.receiveAsFlow().asStream().observe {
+ threadName = Thread.currentThread().name
+ latch.countDown()
+ }
+
+ channel.sendBlocking(123)
+
+ latch.await(5, TimeUnit.SECONDS)
+
+ assertThat(threadName).startsWith("main")
+ }
+ }
+}
diff --git a/orbit-2-core/src/test/java/com/babylon/orbit2/ReducerOrderingTest.kt b/orbit-2-core/src/test/java/com/babylon/orbit2/ReducerOrderingTest.kt
index baa14064..9d37fe44 100644
--- a/orbit-2-core/src/test/java/com/babylon/orbit2/ReducerOrderingTest.kt
+++ b/orbit-2-core/src/test/java/com/babylon/orbit2/ReducerOrderingTest.kt
@@ -28,7 +28,7 @@ internal class ReducerOrderingTest {
fun `reductions are applied in sequence`() {
runBlocking {
val middleware = ThreeReducersMiddleware()
- val testStateObserver = middleware.container.stateStream.test()
+ val testStateObserver = middleware.container.stateFlow.test()
val expectedStates = mutableListOf(
TestState(
emptyList()
@@ -51,7 +51,7 @@ internal class ReducerOrderingTest {
testStateObserver.awaitCount(1120)
- assertThat(testStateObserver.values).containsExactlyElementsOf(expectedStates)
+ assertThat(testStateObserver.values.last()).isEqualTo(expectedStates.last())
}
}
diff --git a/orbit-2-core/src/test/java/com/babylon/orbit2/SideEffectTest.kt b/orbit-2-core/src/test/java/com/babylon/orbit2/SideEffectTest.kt
index d3f2fc8a..3de3805a 100644
--- a/orbit-2-core/src/test/java/com/babylon/orbit2/SideEffectTest.kt
+++ b/orbit-2-core/src/test/java/com/babylon/orbit2/SideEffectTest.kt
@@ -19,74 +19,52 @@ package com.babylon.orbit2
import com.appmattus.kotlinfixture.kotlinFixture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.extension.ExtensionContext
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.Arguments
-import org.junit.jupiter.params.provider.ArgumentsProvider
-import org.junit.jupiter.params.provider.ArgumentsSource
-import java.util.stream.Stream
internal class SideEffectTest {
private val fixture = kotlinFixture()
- object MulticastTestCases : ArgumentsProvider {
- override fun provideArguments(context: ExtensionContext?): Stream =
- Stream.of(
- Arguments.of(true),
- Arguments.of(false),
- Arguments.of(null)
- )
- }
-
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(MulticastTestCases::class)
- fun `side effects are multicast to all current observers by default`(caching: Boolean?) {
+ @Test
+ fun `side effects are not multicast`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(caching)
+ val middleware = Middleware()
- val testSideEffectObserver1 = middleware.container.sideEffectStream.test()
- val testSideEffectObserver2 = middleware.container.sideEffectStream.test()
- val testSideEffectObserver3 = middleware.container.sideEffectStream.test()
+ val testSideEffectObserver1 = middleware.container.sideEffectFlow.test()
+ val testSideEffectObserver2 = middleware.container.sideEffectFlow.test()
+ val testSideEffectObserver3 = middleware.container.sideEffectFlow.test()
middleware.someFlow(action)
middleware.someFlow(action2)
middleware.someFlow(action3)
- testSideEffectObserver1.awaitCount(3)
- testSideEffectObserver2.awaitCount(3)
- testSideEffectObserver3.awaitCount(3)
+ val timeout = 500L
+ testSideEffectObserver1.awaitCount(3, timeout)
+ testSideEffectObserver2.awaitCount(3, timeout)
+ testSideEffectObserver3.awaitCount(3, timeout)
- assertThat(testSideEffectObserver1.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver2.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver3.values).containsExactly(action, action2, action3)
+ assertThat(testSideEffectObserver1.values).doesNotContainSequence(action, action2, action3)
+ assertThat(testSideEffectObserver2.values).doesNotContainSequence(action, action2, action3)
+ assertThat(testSideEffectObserver3.values).doesNotContainSequence(action, action2, action3)
}
- object CachingOnTestCases : ArgumentsProvider {
- override fun provideArguments(context: ExtensionContext?): Stream =
- Stream.of(
- Arguments.of(null),
- Arguments.of(true)
- )
- }
-
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(CachingOnTestCases::class)
- fun `when caching is turned on side effects are cached when there are no subscribers`(caching: Boolean?) {
+ @Test
+ fun `side effects are cached when there are no subscribers`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(caching)
+ val middleware = Middleware()
middleware.someFlow(action)
middleware.someFlow(action2)
middleware.someFlow(action3)
- val testSideEffectObserver1 = middleware.container.sideEffectStream.test()
+ val testSideEffectObserver1 = middleware.container.sideEffectFlow.test()
testSideEffectObserver1.awaitCount(3)
@@ -94,12 +72,12 @@ internal class SideEffectTest {
}
@Test
- fun `when caching is turned off side effects are not cached when there are no subscribers`() {
+ fun `consumed side effects are not resent`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(false)
- val testSideEffectObserver1 = middleware.container.sideEffectStream.test()
+ val middleware = Middleware()
+ val testSideEffectObserver1 = middleware.container.sideEffectFlow.test()
middleware.someFlow(action)
middleware.someFlow(action2)
@@ -107,70 +85,42 @@ internal class SideEffectTest {
testSideEffectObserver1.awaitCount(3)
testSideEffectObserver1.close()
- val testSideEffectObserver2 = middleware.container.sideEffectStream.test()
+ val testSideEffectObserver2 = middleware.container.sideEffectFlow.test()
testSideEffectObserver1.awaitCount(3, 10L)
assertThat(testSideEffectObserver2.values).isEmpty()
}
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(CachingOnTestCases::class)
- fun `when caching is turned on only new side effects are emitted when resubscribing`(caching: Boolean?) {
+ @Test
+ fun `only new side effects are emitted when resubscribing`() {
val action = fixture()
- val action2 = fixture()
- val action3 = fixture()
- val middleware = Middleware(caching)
+ val middleware = Middleware()
- val testSideEffectObserver1 = middleware.container.sideEffectStream.test()
+ val testSideEffectObserver1 = middleware.container.sideEffectFlow.test()
middleware.someFlow(action)
testSideEffectObserver1.awaitCount(1)
testSideEffectObserver1.close()
- middleware.someFlow(action2)
- middleware.someFlow(action3)
-
- val testSideEffectObserver2 = middleware.container.sideEffectStream.test()
- testSideEffectObserver2.awaitCount(2)
-
- assertThat(testSideEffectObserver1.values).containsExactly(action)
- assertThat(testSideEffectObserver2.values).containsExactly(action2, action3)
- }
-
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(CachingOnTestCases::class)
- fun `when caching is turned on new subscribers do not get updates if there is already a sub`(
- caching: Boolean?
- ) {
- val action = fixture()
- val action2 = fixture()
- val action3 = fixture()
- val middleware = Middleware(caching)
-
- val testSideEffectObserver1 = middleware.container.sideEffectStream.test()
-
- middleware.someFlow(action)
- middleware.someFlow(action2)
- middleware.someFlow(action3)
+ GlobalScope.launch {
+ repeat(1000) {
+ middleware.someFlow(it)
+ }
+ }
- testSideEffectObserver1.awaitCount(3)
+ Thread.sleep(200)
- val testSideEffectObserver2 = middleware.container.sideEffectStream.test()
+ val testSideEffectObserver2 = middleware.container.sideEffectFlow.test()
+ testSideEffectObserver2.awaitCount(1000)
- assertThat(testSideEffectObserver1.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver2.values).isEmpty()
+ assertThat(testSideEffectObserver1.values).containsExactly(action)
+ assertThat(testSideEffectObserver2.values).containsExactlyElementsOf((0..999).toList())
}
- private class Middleware(caching: Boolean? = null) : ContainerHost {
- override val container: Container =
- with(CoroutineScope(Dispatchers.Unconfined)) {
- when (caching) {
- null -> container(Unit) // making sure defaults are tested
- else -> container(Unit, Container.Settings(caching))
- }
- }
+ private class Middleware : ContainerHost {
+ override val container: Container = CoroutineScope(Dispatchers.Unconfined).container(Unit)
fun someFlow(action: Int) = orbit {
sideEffect {
diff --git a/orbit-2-core/src/test/java/com/babylon/orbit2/StateConnectionTest.kt b/orbit-2-core/src/test/java/com/babylon/orbit2/StateConnectionTest.kt
index 8b409601..67a436ac 100644
--- a/orbit-2-core/src/test/java/com/babylon/orbit2/StateConnectionTest.kt
+++ b/orbit-2-core/src/test/java/com/babylon/orbit2/StateConnectionTest.kt
@@ -30,7 +30,7 @@ internal class StateConnectionTest {
fun `initial state is emitted on connection`() {
val initialState = fixture()
val middleware = Middleware(initialState)
- val testStateObserver = middleware.container.stateStream.test()
+ val testStateObserver = middleware.container.stateFlow.test()
testStateObserver.awaitCount(1)
@@ -41,12 +41,12 @@ internal class StateConnectionTest {
fun `latest state is emitted on connection`() {
val initialState = fixture()
val middleware = Middleware(initialState)
- val testStateObserver = middleware.container.stateStream.test()
+ val testStateObserver = middleware.container.stateFlow.test()
val action = fixture()
middleware.something(action)
testStateObserver.awaitCount(2) // block until the state is updated
- val testStateObserver2 = middleware.container.stateStream.test()
+ val testStateObserver2 = middleware.container.stateFlow.test()
testStateObserver2.awaitCount(1)
assertThat(testStateObserver.values).containsExactly(
@@ -73,7 +73,7 @@ internal class StateConnectionTest {
val initialState = fixture()
val middleware = Middleware(initialState)
val action = fixture()
- val testStateObserver = middleware.container.stateStream.test()
+ val testStateObserver = middleware.container.stateFlow.test()
middleware.something(action)
diff --git a/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginBehaviourTest.kt b/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginBehaviourTest.kt
index a274c5e3..ac14cb21 100644
--- a/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginBehaviourTest.kt
+++ b/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginBehaviourTest.kt
@@ -22,6 +22,7 @@ import com.babylon.orbit2.OrbitDslPlugins
import com.babylon.orbit2.assert
import com.babylon.orbit2.container
import com.babylon.orbit2.reduce
+import com.babylon.orbit2.sideEffect
import com.babylon.orbit2.test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -90,11 +91,11 @@ internal class CoroutineDslPluginBehaviourTest {
channel.sendBlocking(action + 3)
middleware.assert {
- states(
- { TestState(action) },
- { TestState(action + 1) },
- { TestState(action + 2) },
- { TestState(action + 3) }
+ postedSideEffects(
+ action.toString(),
+ (action + 1).toString(),
+ (action + 2).toString(),
+ (action + 3).toString()
)
}
}
@@ -129,8 +130,8 @@ internal class CoroutineDslPluginBehaviourTest {
transformFlow {
hotFlow
}
- .reduce {
- state.copy(id = event)
+ .sideEffect {
+ post(event.toString())
}
}
}
diff --git a/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginThreadingTest.kt b/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginThreadingTest.kt
index e80b6831..505a4a31 100644
--- a/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginThreadingTest.kt
+++ b/orbit-2-coroutines/src/test/java/com/babylon/orbit2/coroutines/CoroutineDslPluginThreadingTest.kt
@@ -44,7 +44,7 @@ internal class CoroutineDslPluginThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.suspend(action)
@@ -57,7 +57,7 @@ internal class CoroutineDslPluginThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.flow(action)
diff --git a/orbit-2-livedata/README.md b/orbit-2-livedata/README.md
index fc6d67c1..37cba4c4 100644
--- a/orbit-2-livedata/README.md
+++ b/orbit-2-livedata/README.md
@@ -21,9 +21,8 @@ extension properties:
- [state](src/main/java/com/babylon/orbit2/livedata/LiveDataPlugin.kt#state)
- [sideEffect](src/main/java/com/babylon/orbit2/livedata/LiveDataPlugin.kt#sideEffect)
-Below is the recommended way to subscribe to a
-[Container](../orbit-2-core/src/main/java/com/babylon/orbit2/Container.kt) in
-Android.
+These extensions will be removed in Orbit 1.2.0 due to fundamental
+incompatibility of `LiveData` design with Orbit design goals.
``` kotlin
class ExampleActivity: AppCompatActivity() {
@@ -34,8 +33,8 @@ class ExampleActivity: AppCompatActivity() {
override fun onCreate(savedState: Bundle?) {
...
- viewModel.container.state.observe(this, Observer {render(it) })
- viewModel.container.sideEffect.observe(this, Observer {handleSideEffect(it) })
+ viewModel.container.state.observe(this) {render(it) }
+ viewModel.container.sideEffect.observe(this) {handleSideEffect(it) }
}
private fun render(state: CalculatorState) {
diff --git a/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/DelegatingLiveData.kt b/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/DelegatingLiveData.kt
index 155f7da3..7c410258 100644
--- a/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/DelegatingLiveData.kt
+++ b/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/DelegatingLiveData.kt
@@ -20,28 +20,28 @@ import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
-import com.babylon.orbit2.Stream
-import java.io.Closeable
+import androidx.lifecycle.asLiveData
+import kotlinx.coroutines.flow.Flow
/**
- * This class creates one LiveData per observer in order to defer to the behaviour of the [Stream]
+ * This class creates one LiveData per observer in order to defer to the behaviour of the [Flow]
* when it comes to caching values. This ensures that side effect caching is properly
* resolved while retaining the benefits of using LiveData in terms of main thread callbacks and
* automatic unsubscription.
*/
-internal class DelegatingLiveData(private val stream: Stream) : LiveData() {
+internal class DelegatingLiveData(private val flow: Flow) : LiveData() {
private val closeables = mutableMapOf, LiveData>()
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer) {
// Observe the internal MutableLiveData
- closeables[observer] = InternalStreamLiveData(stream).also {
+ closeables[observer] = flow.asLiveData(timeoutInMs = 0L).also {
it.observe(owner, observer)
}
}
override fun observeForever(observer: Observer) {
- closeables[observer] = InternalStreamLiveData(stream).also {
+ closeables[observer] = flow.asLiveData(timeoutInMs = 0L).also {
it.observeForever(observer)
}
}
@@ -69,18 +69,4 @@ internal class DelegatingLiveData(private val stream: Stream) : LiveData(private val stream: Stream) : LiveData() {
- private var closeable: Closeable? = null
-
- override fun onActive() {
- closeable = stream.observe {
- postValue(it)
- }
- }
-
- override fun onInactive() {
- closeable?.close()
- }
- }
}
diff --git a/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/LiveDataPlugin.kt b/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/LiveDataPlugin.kt
index ac0624f1..47ffaa0b 100644
--- a/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/LiveDataPlugin.kt
+++ b/orbit-2-livedata/src/main/java/com/babylon/orbit2/livedata/LiveDataPlugin.kt
@@ -17,32 +17,26 @@
package com.babylon.orbit2.livedata
import androidx.lifecycle.LiveData
+import androidx.lifecycle.asLiveData
import com.babylon.orbit2.Container
import com.babylon.orbit2.Container.Settings
-import java.io.Closeable
/**
* A [LiveData] of one-off side effects. Depending on the [Settings] this container has been
* instantiated with, can support side effect caching when there are no listeners (default)
*/
+@Deprecated(
+ message = "Please use sideEffectFlow instead. Will be removed in Orbit 1.2.0"
+)
val Container.sideEffect: LiveData
- get() = DelegatingLiveData(this.sideEffectStream)
+ get() = DelegatingLiveData(sideEffectFlow)
/**
* A [LiveData] of state updates. Emits the latest state upon subscription and serves only distinct
* values (only changed states are emitted) by default.
*/
+@Deprecated(
+ message = "Please use stateFlow instead. Will be removed in Orbit 1.2.0"
+)
val Container.state: LiveData
- get() = object : LiveData(this.currentState) {
- private var closeable: Closeable? = null
-
- override fun onActive() {
- closeable = this@state.stateStream.observe {
- postValue(it)
- }
- }
-
- override fun onInactive() {
- closeable?.close()
- }
- }
+ get() = stateFlow.asLiveData()
diff --git a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/DelegatingLiveDataTest.kt b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/DelegatingLiveDataTest.kt
index 748ac9eb..b8f1430f 100644
--- a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/DelegatingLiveDataTest.kt
+++ b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/DelegatingLiveDataTest.kt
@@ -18,14 +18,26 @@ package com.babylon.orbit2.livedata
import androidx.lifecycle.Lifecycle
import com.appmattus.kotlinfixture.kotlinFixture
-import com.babylon.orbit2.Stream
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.sendBlocking
+import kotlinx.coroutines.flow.consumeAsFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
-import java.io.Closeable
+@ExperimentalCoroutinesApi
@ExtendWith(InstantTaskExecutorExtension::class)
internal class DelegatingLiveDataTest {
private val fixture = kotlinFixture()
@@ -33,19 +45,14 @@ internal class DelegatingLiveDataTest {
it.dispatchEvent(Lifecycle.Event.ON_CREATE)
}
- class TestStream : Stream {
- private val observers = mutableSetOf<(T) -> Unit>()
-
- override fun observe(lambda: (T) -> Unit): Closeable {
- observers += lambda
- return Closeable { observers.remove(lambda) }
- }
-
- fun post(value: T) {
- observers.forEach { it(value) }
- }
+ @BeforeEach
+ fun beforeEach() {
+ Dispatchers.setMain(Dispatchers.Unconfined)
+ }
- fun hasObservers() = observers.size > 0
+ @AfterEach
+ fun afterEach() {
+ Dispatchers.resetMain()
}
@Suppress("unused")
@@ -78,58 +85,57 @@ internal class DelegatingLiveDataTest {
@ParameterizedTest
@EnumSource(TestCase::class)
fun `observer does not subscribe until onStart`(testCase: TestCase) {
- val stream = TestStream()
+ val channel = Channel()
val action = fixture()
mockLifecycleOwner.currentState = testCase.state
- val observer = DelegatingLiveData(stream).test(mockLifecycleOwner)
+ val observer = DelegatingLiveData(channel.consumeAsFlow()).test(mockLifecycleOwner)
- stream.post(action)
+ GlobalScope.launch {
+ channel.send(action)
+ }
if (testCase.expectedSubscription) {
- assertThat(stream.hasObservers()).isTrue()
+ observer.awaitCount(1)
assertThat(observer.values).containsExactly(action)
} else {
- assertThat(stream.hasObservers()).isFalse()
assertThat(observer.values).isEmpty()
}
}
@Test
fun `observer is unsubscribed after the lifecycle is stopped`() {
- val stream = TestStream()
+ val channel = Channel()
val action = fixture()
val action2 = fixture()
- val action3 = fixture()
- val observer = DelegatingLiveData(stream).test(mockLifecycleOwner)
+ val observer = DelegatingLiveData(channel.consumeAsFlow()).test(mockLifecycleOwner)
mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_START)
mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_RESUME)
- stream.post(action)
- assertThat(stream.hasObservers()).isTrue()
+ channel.sendBlocking(action)
mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_PAUSE)
mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_STOP)
- stream.post(action2)
- stream.post(action3)
+ assertThrows {
+ channel.sendBlocking(action2)
+ }
assertThat(observer.values).containsExactly(action)
- assertThat(stream.hasObservers()).isFalse()
}
@Test
fun `the current value cannot be retrieved and returns nulls instead`() {
- val stream = TestStream()
+ val channel = Channel()
val action = fixture()
- val liveData = DelegatingLiveData(stream)
+ val liveData = DelegatingLiveData(channel.consumeAsFlow())
val observer = liveData.test(mockLifecycleOwner)
mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_START)
mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_RESUME)
- stream.post(action)
+ channel.sendBlocking(action)
assertThat(observer.values).containsExactly(action)
assertThat(liveData.value).isNull()
diff --git a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/LiveDataDslPluginDslThreadingTest.kt b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/LiveDataDslPluginDslThreadingTest.kt
index 3150a339..929b5abe 100644
--- a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/LiveDataDslPluginDslThreadingTest.kt
+++ b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/LiveDataDslPluginDslThreadingTest.kt
@@ -61,7 +61,7 @@ internal class LiveDataDslPluginDslThreadingTest {
val action = fixture()
val container = scope.createContainer()
- val sideEffects = container.sideEffectStream.test()
+ val sideEffects = container.sideEffectFlow.test()
var threadName = ""
container.orbit {
diff --git a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/SideEffectLiveDataPluginTest.kt b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/SideEffectLiveDataPluginTest.kt
index c42fc67d..370f27ab 100644
--- a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/SideEffectLiveDataPluginTest.kt
+++ b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/SideEffectLiveDataPluginTest.kt
@@ -24,16 +24,17 @@ import com.babylon.orbit2.container
import com.babylon.orbit2.sideEffect
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import org.junit.jupiter.api.extension.ExtensionContext
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.Arguments
-import org.junit.jupiter.params.provider.ArgumentsProvider
-import org.junit.jupiter.params.provider.ArgumentsSource
-import java.util.stream.Stream
+@Suppress("DEPRECATION")
+@ExperimentalCoroutinesApi
@ExtendWith(InstantTaskExecutorExtension::class)
internal class SideEffectLiveDataPluginTest {
private val fixture = kotlinFixture()
@@ -42,63 +43,47 @@ internal class SideEffectLiveDataPluginTest {
it.dispatchEvent(Lifecycle.Event.ON_START)
}
- internal object MulticastTestCases : ArgumentsProvider {
- override fun provideArguments(context: ExtensionContext?): Stream =
- Stream.of(
- Arguments.of(true),
- Arguments.of(false),
- Arguments.of(null)
- )
+ @BeforeEach
+ fun beforeEach() {
+ Dispatchers.setMain(Dispatchers.Unconfined)
}
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(MulticastTestCases::class)
- fun `side effects are multicast to all current observers using separate livedatas`(
- enabled: Boolean?
- ) {
+ @AfterEach
+ fun afterEach() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `side effects are not multicast to observers using separate livedatas`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(enabled)
- val mockLifecycleOwner = MockLifecycleOwner()
- mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_CREATE)
- mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_START)
-
- val testSideEffectObserver1 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
- val testSideEffectObserver2 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
- val testSideEffectObserver3 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ val middleware = Middleware()
+ val testSideEffectObserver1 = middleware.container.sideEffect.test(mockLifecycleOwner)
+ val testSideEffectObserver2 = middleware.container.sideEffect.test(mockLifecycleOwner)
+ val testSideEffectObserver3 = middleware.container.sideEffect.test(mockLifecycleOwner)
middleware.someFlow(action)
middleware.someFlow(action2)
middleware.someFlow(action3)
- testSideEffectObserver1.awaitCount(3)
- testSideEffectObserver2.awaitCount(3)
- testSideEffectObserver3.awaitCount(3)
+ val timeout = 500L
+ testSideEffectObserver1.awaitCount(3, timeout)
+ testSideEffectObserver2.awaitCount(3, timeout)
+ testSideEffectObserver3.awaitCount(3, timeout)
- assertThat(testSideEffectObserver1.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver2.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver3.values).containsExactly(action, action2, action3)
+ assertThat(testSideEffectObserver1.values).doesNotContainSequence(action, action2, action3)
+ assertThat(testSideEffectObserver2.values).doesNotContainSequence(action, action2, action3)
+ assertThat(testSideEffectObserver3.values).doesNotContainSequence(action, action2, action3)
}
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(MulticastTestCases::class)
- fun `side effects are multicast to all current observers using a single livedata`(
- enabled: Boolean?
- ) {
+ @Test
+ fun `side effects are not multicast to all current observers using a single livedata`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(enabled)
- val mockLifecycleOwner = MockLifecycleOwner()
- mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_CREATE)
- mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_START)
-
+ val middleware = Middleware()
val liveData = middleware.container.sideEffect
-
val testSideEffectObserver1 =
liveData.test(mockLifecycleOwner)
val testSideEffectObserver2 =
@@ -110,37 +95,28 @@ internal class SideEffectLiveDataPluginTest {
middleware.someFlow(action2)
middleware.someFlow(action3)
- testSideEffectObserver1.awaitCount(3)
- testSideEffectObserver2.awaitCount(3)
- testSideEffectObserver3.awaitCount(3)
+ val timeout = 500L
+ testSideEffectObserver1.awaitCount(3, timeout)
+ testSideEffectObserver2.awaitCount(3, timeout)
+ testSideEffectObserver3.awaitCount(3, timeout)
- assertThat(testSideEffectObserver1.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver2.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver3.values).containsExactly(action, action2, action3)
- }
-
- object CachingOnTestCases : ArgumentsProvider {
- override fun provideArguments(context: ExtensionContext?): Stream =
- Stream.of(
- Arguments.of(null),
- Arguments.of(true)
- )
+ assertThat(testSideEffectObserver1.values).doesNotContainSequence(action, action2, action3)
+ assertThat(testSideEffectObserver2.values).doesNotContainSequence(action, action2, action3)
+ assertThat(testSideEffectObserver3.values).doesNotContainSequence(action, action2, action3)
}
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(CachingOnTestCases::class)
- fun `caching on - side effects are cached when there are no subscribers`(caching: Boolean?) {
+ @Test
+ fun `side effects are cached when there are no subscribers`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(caching)
+ val middleware = Middleware()
middleware.someFlow(action)
middleware.someFlow(action2)
middleware.someFlow(action3)
- val testSideEffectObserver1 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ val testSideEffectObserver1 = middleware.container.sideEffect.test(mockLifecycleOwner)
testSideEffectObserver1.awaitCount(3)
@@ -148,132 +124,97 @@ internal class SideEffectLiveDataPluginTest {
}
@Test
- fun `caching off - side effects are not cached when there are no subscribers`() {
+ fun `observer is unsubscribed after onDestroy`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(false)
- val testSideEffectObserver1 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ val middleware = Middleware()
+ val testSideEffectObserver1 = middleware.container.sideEffect.test(mockLifecycleOwner)
middleware.someFlow(action)
- middleware.someFlow(action2)
- middleware.someFlow(action3)
- testSideEffectObserver1.awaitCount(3)
- testSideEffectObserver1.close()
+ testSideEffectObserver1.awaitCount(1)
- val testSideEffectObserver2 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_DESTROY)
- testSideEffectObserver2.awaitCount(3, 10L)
+ middleware.someFlow(action2)
+ middleware.someFlow(action3)
- assertThat(testSideEffectObserver2.values).isEmpty()
+ testSideEffectObserver1.awaitCount(3, 500L)
+ assertThat(testSideEffectObserver1.values).containsExactly(action)
}
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(CachingOnTestCases::class)
- fun `caching on - only new side effects are emitted when resubscribing to the same live data`(
- caching: Boolean?
- ) {
+ @Test
+ fun `new subscribers do not get updates if there is already a sub`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(caching)
-
+ val middleware = Middleware()
val liveData = middleware.container.sideEffect
-
val testSideEffectObserver1 = liveData.test(mockLifecycleOwner)
middleware.someFlow(action)
-
- testSideEffectObserver1.awaitCount(1)
- testSideEffectObserver1.close()
-
middleware.someFlow(action2)
middleware.someFlow(action3)
+ testSideEffectObserver1.awaitCount(3)
+
val testSideEffectObserver2 = liveData.test(mockLifecycleOwner)
- testSideEffectObserver2.awaitCount(2)
- assertThat(testSideEffectObserver1.values).containsExactly(action)
- assertThat(testSideEffectObserver2.values).containsExactly(action2, action3)
+ assertThat(testSideEffectObserver1.values).containsExactly(action, action2, action3)
+ assertThat(testSideEffectObserver2.values).isEmpty()
}
@Test
- fun `caching off - only new side effects are emitted when resubscribing to the same live data`() {
+ fun `side effects are not conflated`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(false)
-
- val liveData = middleware.container.sideEffect
-
- val testSideEffectObserver1 = liveData.test(mockLifecycleOwner)
+ val middleware = Middleware()
+ val testSideEffectObserver = middleware.container.sideEffect.test(mockLifecycleOwner)
middleware.someFlow(action)
-
- testSideEffectObserver1.awaitCount(1)
- testSideEffectObserver1.close()
-
- val testSideEffectObserver2 = liveData.test(mockLifecycleOwner)
-
middleware.someFlow(action2)
middleware.someFlow(action3)
- testSideEffectObserver2.awaitCount(2)
- assertThat(testSideEffectObserver1.values).containsExactly(action)
- assertThat(testSideEffectObserver2.values).containsExactly(action2, action3)
+ testSideEffectObserver.awaitCount(3)
+
+ assertThat(testSideEffectObserver.values).containsExactly(action, action2, action3)
}
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(CachingOnTestCases::class)
- fun `caching on - only new side effects are emitted when resubscribing to different live datas`(
- caching: Boolean?
- ) {
+ @Test
+ fun `consecutive equal objects are emitted properly`() {
val action = fixture()
- val action2 = fixture()
- val action3 = fixture()
- val middleware = Middleware(caching)
-
- val testSideEffectObserver1 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ val middleware = Middleware()
+ val testSideEffectObserver = middleware.container.sideEffect.test(mockLifecycleOwner)
+ middleware.someFlow(action)
+ middleware.someFlow(action)
middleware.someFlow(action)
- testSideEffectObserver1.awaitCount(1)
- testSideEffectObserver1.close()
-
- middleware.someFlow(action2)
- middleware.someFlow(action3)
-
- val testSideEffectObserver2 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
- testSideEffectObserver2.awaitCount(2)
+ testSideEffectObserver.awaitCount(3)
- assertThat(testSideEffectObserver1.values).containsExactly(action)
- assertThat(testSideEffectObserver2.values).containsExactly(action2, action3)
+ assertThat(testSideEffectObserver.values).containsExactly(action, action, action)
}
@Test
- fun `caching off - only new side effects are emitted when resubscribing to different live datas`() {
+ fun `only new side effects are emitted when resubscribing to the same live data`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(false)
-
- val testSideEffectObserver1 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ val middleware = Middleware()
+ val liveData = middleware.container.sideEffect
+ val testSideEffectObserver1 = liveData.test(mockLifecycleOwner)
middleware.someFlow(action)
testSideEffectObserver1.awaitCount(1)
testSideEffectObserver1.close()
-
- val testSideEffectObserver2 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ testSideEffectObserver1.awaitNoActiveObservers()
middleware.someFlow(action2)
middleware.someFlow(action3)
+
+ val testSideEffectObserver2 = liveData.test(mockLifecycleOwner)
testSideEffectObserver2.awaitCount(2)
assertThat(testSideEffectObserver1.values).containsExactly(action)
@@ -281,83 +222,58 @@ internal class SideEffectLiveDataPluginTest {
}
@Test
- fun `observer is unsubscribed after onDestroy`() {
+ fun `only new side effects are emitted when resubscribing to different live datas`() {
val action = fixture()
val action2 = fixture()
val action3 = fixture()
- val middleware = Middleware(false)
-
- val testSideEffectObserver1 =
- middleware.container.sideEffect.test(mockLifecycleOwner)
+ val middleware = Middleware()
+ val testSideEffectObserver1 = middleware.container.sideEffect.test(mockLifecycleOwner)
middleware.someFlow(action)
- testSideEffectObserver1.awaitCount(1)
- mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_DESTROY)
+ testSideEffectObserver1.awaitCount(1)
+ testSideEffectObserver1.close()
+ testSideEffectObserver1.awaitNoActiveObservers()
middleware.someFlow(action2)
middleware.someFlow(action3)
+ val testSideEffectObserver2 =
+ middleware.container.sideEffect.test(mockLifecycleOwner)
+ testSideEffectObserver2.awaitCount(2)
+
assertThat(testSideEffectObserver1.values).containsExactly(action)
+ assertThat(testSideEffectObserver2.values).containsExactly(action2, action3)
}
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(CachingOnTestCases::class)
- fun `when caching is turned on new subscribers do not get updates if there is already a sub`(
- caching: Boolean?
- ) {
+ @Test
+ fun `side effects are delivered in two different subscriptions`() {
val action = fixture()
- val action2 = fixture()
- val action3 = fixture()
- val middleware = Middleware(caching)
- val liveData = middleware.container.sideEffect
-
- val testSideEffectObserver1 = liveData.test(mockLifecycleOwner)
+ val middleware = Middleware()
+ val testSideEffectObserver = middleware.container.sideEffect.test(mockLifecycleOwner)
middleware.someFlow(action)
- middleware.someFlow(action2)
- middleware.someFlow(action3)
+ testSideEffectObserver.awaitCount(1)
- testSideEffectObserver1.awaitCount(3)
+ mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_STOP)
+ testSideEffectObserver.close()
+ testSideEffectObserver.awaitNoActiveObservers()
- val testSideEffectObserver2 = liveData.test(mockLifecycleOwner)
+ assertThat(testSideEffectObserver.values).containsExactly(action)
- assertThat(testSideEffectObserver1.values).containsExactly(action, action2, action3)
- assertThat(testSideEffectObserver2.values).isEmpty()
- }
-
- @ParameterizedTest(name = "Caching is {0}")
- @ArgumentsSource(MulticastTestCases::class)
- fun `side effects are not conflated`(
- enabled: Boolean?
- ) {
- val action = fixture()
- val action2 = fixture()
- val action3 = fixture()
- val middleware = Middleware(enabled)
- val mockLifecycleOwner = MockLifecycleOwner()
- mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_CREATE)
mockLifecycleOwner.dispatchEvent(Lifecycle.Event.ON_START)
-
- val testSideEffectObserver = middleware.container.sideEffect.test(mockLifecycleOwner)
+ val testSideEffectObserver2 = middleware.container.sideEffect.test(mockLifecycleOwner)
middleware.someFlow(action)
- middleware.someFlow(action2)
- middleware.someFlow(action3)
+ middleware.someFlow(action + 1)
- testSideEffectObserver.awaitCount(3)
+ testSideEffectObserver2.awaitCount(2)
- assertThat(testSideEffectObserver.values).containsExactly(action, action2, action3)
+ assertThat(testSideEffectObserver2.values).containsExactly(action, action + 1)
}
- private class Middleware(caching: Boolean? = null) : ContainerHost {
- override val container: Container =
- with(CoroutineScope(Dispatchers.Unconfined)) {
- when (caching) {
- null -> container(Unit) // making sure defaults are tested
- else -> container(Unit, Container.Settings(caching))
- }
- }
+ private class Middleware : ContainerHost {
+ override val container: Container = CoroutineScope(Dispatchers.Unconfined).container(Unit)
fun someFlow(action: Int) = orbit {
sideEffect {
diff --git a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/StateConnectionLiveDataPluginTest.kt b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/StateConnectionLiveDataPluginTest.kt
index 5ab7fd61..67a54ffb 100644
--- a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/StateConnectionLiveDataPluginTest.kt
+++ b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/StateConnectionLiveDataPluginTest.kt
@@ -23,10 +23,17 @@ import com.babylon.orbit2.container
import com.babylon.orbit2.reduce
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
+@Suppress("DEPRECATION")
+@ExperimentalCoroutinesApi
@ExtendWith(InstantTaskExecutorExtension::class)
internal class StateConnectionLiveDataPluginTest {
@@ -36,6 +43,16 @@ internal class StateConnectionLiveDataPluginTest {
dispatchEvent(Lifecycle.Event.ON_START)
}
+ @BeforeEach
+ fun beforeEach() {
+ Dispatchers.setMain(Dispatchers.Unconfined)
+ }
+
+ @AfterEach
+ fun afterEach() {
+ Dispatchers.resetMain()
+ }
+
@Test
fun `initial state is emitted on connection`() {
val initialState = fixture()
diff --git a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/TestLiveDataObserver.kt b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/TestLiveDataObserver.kt
index c2f145a1..94db0d64 100644
--- a/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/TestLiveDataObserver.kt
+++ b/orbit-2-livedata/src/test/java/com/babylon/orbit2/livedata/TestLiveDataObserver.kt
@@ -45,5 +45,15 @@ class TestLiveDataObserver(lifecycleOwner: LifecycleOwner, private val liveDa
}
}
+ fun awaitNoActiveObservers(timeout: Long = 5000L) {
+ val start = System.currentTimeMillis()
+ while (liveData.hasObservers()) {
+ if (System.currentTimeMillis() - start > timeout) {
+ break
+ }
+ Thread.sleep(10)
+ }
+ }
+
fun close(): Unit = liveData.removeObserver(observer)
}
diff --git a/orbit-2-rxjava1/src/main/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensions.kt b/orbit-2-rxjava1/src/main/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensions.kt
index dc6a38d9..34a10c9b 100644
--- a/orbit-2-rxjava1/src/main/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensions.kt
+++ b/orbit-2-rxjava1/src/main/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensions.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION")
+
package com.babylon.orbit2.rxjava1
import com.babylon.orbit2.Stream
@@ -24,6 +26,9 @@ import java.util.concurrent.atomic.AtomicBoolean
/**
* Consume a [Stream] as an RxJava 1 [Observable].
*/
+@Deprecated(
+ message = "Stream is deprecated. Please consider upgrading to RxJava 2 or 3 or using Container.stateFlow or Container.sideEffectFlow.",
+)
fun Stream.asRx1Observable() = Observable.unsafeCreate { emitter ->
val unsubscribed = AtomicBoolean(false)
val closeable = observe {
diff --git a/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensionsKtTest.kt b/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensionsKtTest.kt
index 8c444446..ed8eeb70 100644
--- a/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensionsKtTest.kt
+++ b/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/Rx1StreamExtensionsKtTest.kt
@@ -1,3 +1,5 @@
+@file:Suppress("DEPRECATION")
+
package com.babylon.orbit2.rxjava1
import com.appmattus.kotlinfixture.kotlinFixture
diff --git a/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginBehaviourTest.kt b/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginBehaviourTest.kt
index 1c19fcda..5fcb68ef 100644
--- a/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginBehaviourTest.kt
+++ b/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginBehaviourTest.kt
@@ -22,6 +22,7 @@ import com.babylon.orbit2.OrbitDslPlugins
import com.babylon.orbit2.assert
import com.babylon.orbit2.container
import com.babylon.orbit2.reduce
+import com.babylon.orbit2.sideEffect
import com.babylon.orbit2.test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -76,11 +77,11 @@ internal class RxJava1DslPluginBehaviourTest {
middleware.observable(action)
middleware.assert {
- states(
- { TestState(action) },
- { TestState(action + 1) },
- { TestState(action + 2) },
- { TestState(action + 3) }
+ postedSideEffects(
+ action.toString(),
+ (action + 1).toString(),
+ (action + 2).toString(),
+ (action + 3).toString()
)
}
}
@@ -113,8 +114,8 @@ internal class RxJava1DslPluginBehaviourTest {
transformRx1Observable {
Observable.just(action, action + 1, action + 2, action + 3)
}
- .reduce {
- state.copy(id = event)
+ .sideEffect {
+ post(event.toString())
}
}
}
diff --git a/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginDslThreadingTest.kt b/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginDslThreadingTest.kt
index bebdec52..82a2b91d 100644
--- a/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginDslThreadingTest.kt
+++ b/orbit-2-rxjava1/src/test/java/com/babylon/orbit2/rxjava1/RxJava1DslPluginDslThreadingTest.kt
@@ -44,7 +44,7 @@ internal class RxJava1DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.single(action)
@@ -57,7 +57,7 @@ internal class RxJava1DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.completable(action)
@@ -70,7 +70,7 @@ internal class RxJava1DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.observable(action)
diff --git a/orbit-2-rxjava2/orbit-2-rxjava2_build.gradle.kts b/orbit-2-rxjava2/orbit-2-rxjava2_build.gradle.kts
index 6da7eaea..ebb57431 100644
--- a/orbit-2-rxjava2/orbit-2-rxjava2_build.gradle.kts
+++ b/orbit-2-rxjava2/orbit-2-rxjava2_build.gradle.kts
@@ -22,7 +22,7 @@ plugins {
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(ProjectDependencies.rxJava2)
- implementation(ProjectDependencies.kotlinCoroutinesRx2)
+ api(ProjectDependencies.kotlinCoroutinesRx2)
api(project(":orbit-2-core"))
diff --git a/orbit-2-rxjava2/src/main/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensions.kt b/orbit-2-rxjava2/src/main/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensions.kt
index c46832cd..d66fb418 100644
--- a/orbit-2-rxjava2/src/main/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensions.kt
+++ b/orbit-2-rxjava2/src/main/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensions.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION")
+
package com.babylon.orbit2.rxjava2
import com.babylon.orbit2.Stream
@@ -22,6 +24,11 @@ import io.reactivex.Observable
/**
* Consume a [Stream] as an RxJava 2 [Observable].
*/
+@Deprecated(
+ message = "Stream is deprecated. Please use coroutine extensions on " +
+ "Container.stateFlow.asObservable() or Container.sideEffectFlow.asObservable() instead: " +
+ "https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive/kotlinx-coroutines-rx2",
+)
fun Stream.asRx2Observable() =
Observable.create { emitter ->
val closeable = observe {
diff --git a/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensionsKtTest.kt b/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensionsKtTest.kt
index 6f9ac1ba..d6f98224 100644
--- a/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensionsKtTest.kt
+++ b/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/Rx2StreamExtensionsKtTest.kt
@@ -1,3 +1,5 @@
+@file:Suppress("DEPRECATION")
+
package com.babylon.orbit2.rxjava2
import com.appmattus.kotlinfixture.kotlinFixture
diff --git a/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginBehaviourTest.kt b/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginBehaviourTest.kt
index 9dc8c2d1..6eff99cc 100644
--- a/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginBehaviourTest.kt
+++ b/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginBehaviourTest.kt
@@ -22,6 +22,7 @@ import com.babylon.orbit2.OrbitDslPlugins
import com.babylon.orbit2.assert
import com.babylon.orbit2.container
import com.babylon.orbit2.reduce
+import com.babylon.orbit2.sideEffect
import com.babylon.orbit2.test
import io.reactivex.Completable
import io.reactivex.Maybe
@@ -99,11 +100,11 @@ internal class RxJava2DslPluginBehaviourTest {
middleware.observable(action)
middleware.assert {
- states(
- { TestState(action) },
- { TestState(action + 1) },
- { TestState(action + 2) },
- { TestState(action + 3) }
+ postedSideEffects(
+ action.toString(),
+ (action + 1).toString(),
+ (action + 2).toString(),
+ (action + 3).toString()
)
}
}
@@ -154,8 +155,8 @@ internal class RxJava2DslPluginBehaviourTest {
transformRx2Observable {
Observable.just(action, action + 1, action + 2, action + 3)
}
- .reduce {
- state.copy(id = event)
+ .sideEffect {
+ post(event.toString())
}
}
}
diff --git a/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginDslThreadingTest.kt b/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginDslThreadingTest.kt
index 63df98f7..b1a94c17 100644
--- a/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginDslThreadingTest.kt
+++ b/orbit-2-rxjava2/src/test/java/com/babylon/orbit2/rxjava2/RxJava2DslPluginDslThreadingTest.kt
@@ -46,7 +46,7 @@ internal class RxJava2DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.single(action)
@@ -59,7 +59,7 @@ internal class RxJava2DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.maybe(action)
@@ -84,7 +84,7 @@ internal class RxJava2DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.completable(action)
@@ -97,7 +97,7 @@ internal class RxJava2DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.observable(action)
diff --git a/orbit-2-rxjava3/orbit-2-rxjava3_build.gradle.kts b/orbit-2-rxjava3/orbit-2-rxjava3_build.gradle.kts
index b48ef8a1..5686f17a 100644
--- a/orbit-2-rxjava3/orbit-2-rxjava3_build.gradle.kts
+++ b/orbit-2-rxjava3/orbit-2-rxjava3_build.gradle.kts
@@ -22,7 +22,7 @@ plugins {
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(ProjectDependencies.rxJava3)
- implementation(ProjectDependencies.kotlinCoroutinesRx3)
+ api(ProjectDependencies.kotlinCoroutinesRx3)
api(project(":orbit-2-core"))
diff --git a/orbit-2-rxjava3/src/main/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensions.kt b/orbit-2-rxjava3/src/main/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensions.kt
index 2e4af95d..de2278da 100644
--- a/orbit-2-rxjava3/src/main/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensions.kt
+++ b/orbit-2-rxjava3/src/main/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensions.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION")
+
package com.babylon.orbit2.rxjava3
import com.babylon.orbit2.Stream
@@ -22,8 +24,13 @@ import io.reactivex.rxjava3.core.Observable
/**
* Consume a [Stream] as an RxJava 3 [Observable].
*/
-fun Stream.asRx3Observable() =
- Observable.create { emitter ->
+@Deprecated(
+ message = "Stream is deprecated. Please use coroutine extensions on " +
+ "Container.stateFlow.asObservable() or Container.sideEffectFlow.asObservable() instead: " +
+ "https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive/kotlinx-coroutines-rx3",
+)
+fun Stream.asRx3Observable(): Observable =
+ Observable.create { emitter ->
val closeable = observe {
if (!emitter.isDisposed) {
emitter.onNext(it)
diff --git a/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensionsKtTest.kt b/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensionsKtTest.kt
index 8e2ec16d..8184b9ef 100644
--- a/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensionsKtTest.kt
+++ b/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/Rx3StreamExtensionsKtTest.kt
@@ -1,3 +1,5 @@
+@file:Suppress("DEPRECATION")
+
package com.babylon.orbit2.rxjava3
import com.appmattus.kotlinfixture.kotlinFixture
diff --git a/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginBehaviourTest.kt b/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginBehaviourTest.kt
index 3949c7c2..e8b969a3 100644
--- a/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginBehaviourTest.kt
+++ b/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginBehaviourTest.kt
@@ -22,6 +22,7 @@ import com.babylon.orbit2.OrbitDslPlugins
import com.babylon.orbit2.assert
import com.babylon.orbit2.container
import com.babylon.orbit2.reduce
+import com.babylon.orbit2.sideEffect
import com.babylon.orbit2.test
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Maybe
@@ -99,11 +100,11 @@ internal class RxJava3DslPluginBehaviourTest {
middleware.observable(action)
middleware.assert {
- states(
- { TestState(action) },
- { TestState(action + 1) },
- { TestState(action + 2) },
- { TestState(action + 3) }
+ postedSideEffects(
+ action.toString(),
+ (action + 1).toString(),
+ (action + 2).toString(),
+ (action + 3).toString()
)
}
}
@@ -154,8 +155,8 @@ internal class RxJava3DslPluginBehaviourTest {
transformRx3Observable {
Observable.just(action, action + 1, action + 2, action + 3)
}
- .reduce {
- state.copy(id = event)
+ .sideEffect {
+ post(event.toString())
}
}
}
diff --git a/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginDslThreadingTest.kt b/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginDslThreadingTest.kt
index 2b2825ac..e7b495af 100644
--- a/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginDslThreadingTest.kt
+++ b/orbit-2-rxjava3/src/test/java/com/babylon/orbit2/rxjava3/RxJava3DslPluginDslThreadingTest.kt
@@ -46,7 +46,7 @@ internal class RxJava3DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.single(action)
@@ -59,7 +59,7 @@ internal class RxJava3DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.maybe(action)
@@ -84,7 +84,7 @@ internal class RxJava3DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.completable(action)
@@ -97,7 +97,7 @@ internal class RxJava3DslPluginDslThreadingTest {
val action = fixture()
val middleware = Middleware()
- val testStreamObserver = middleware.container.stateStream.test()
+ val testStreamObserver = middleware.container.stateFlow.test()
middleware.observable(action)
diff --git a/orbit-2-test/src/main/java/com/babylon/orbit2/Test.kt b/orbit-2-test/src/main/java/com/babylon/orbit2/Test.kt
index c82c1c85..8cc5dee1 100644
--- a/orbit-2-test/src/main/java/com/babylon/orbit2/Test.kt
+++ b/orbit-2-test/src/main/java/com/babylon/orbit2/Test.kt
@@ -20,6 +20,7 @@ import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -63,8 +64,8 @@ inline fun > T.as
class TestFixtures(
val initialState: STATE,
- val stateObserver: TestStreamObserver,
- val sideEffectObserver: TestStreamObserver
+ val stateObserver: TestFlowObserver,
+ val sideEffectObserver: TestFlowObserver
)
object TestHarness {
@@ -157,4 +158,10 @@ object TestHarness {
/**
* Allows you to put a [Stream] into test mode.
*/
+@Suppress("DEPRECATION")
fun Stream.test() = TestStreamObserver(this)
+
+/**
+ * Allows you to put a [Flow] into test mode.
+ */
+fun Flow.test() = TestFlowObserver(this)
diff --git a/orbit-2-test/src/main/java/com/babylon/orbit2/TestFlowObserver.kt b/orbit-2-test/src/main/java/com/babylon/orbit2/TestFlowObserver.kt
new file mode 100644
index 00000000..7e7ab90f
--- /dev/null
+++ b/orbit-2-test/src/main/java/com/babylon/orbit2/TestFlowObserver.kt
@@ -0,0 +1,70 @@
+package com.babylon.orbit2
+
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/**
+ * Allows you to record all observed values of a flow for easy testing.
+ *
+ * @param flow The flow to observe.
+ */
+class TestFlowObserver(flow: Flow) {
+ private val _values = mutableListOf()
+ private val closeable: Job
+ val values: List
+ get() = _values
+
+ init {
+ closeable = GlobalScope.launch {
+ flow.collect {
+ _values.add(it)
+ }
+ }
+ }
+
+ /**
+ * Awaits until the specified count of elements has been received or the timeout is hit.
+ *
+ * @param count The awaited element count.
+ * @param timeout How long to wait for in milliseconds
+ */
+ fun awaitCount(count: Int, timeout: Long = 5000L) {
+ val start = System.currentTimeMillis()
+ while (values.count() < count) {
+ if (System.currentTimeMillis() - start > timeout) {
+ break
+ }
+ Thread.sleep(AWAIT_TIMEOUT_MS)
+ }
+ }
+
+ /**
+ * Awaits until the specified count of elements has been received or the timeout is hit.
+ *
+ * @param count The awaited element count.
+ * @param timeout How long to wait for in milliseconds
+ */
+ suspend fun awaitCountSuspending(count: Int, timeout: Long = 5000L) {
+ val start = System.currentTimeMillis()
+ while (values.count() < count) {
+ if (System.currentTimeMillis() - start > timeout) {
+ break
+ }
+ delay(AWAIT_TIMEOUT_MS)
+ }
+ }
+
+ /**
+ * Closes the subscription on the underlying stream. No further values will be received after
+ * this call.
+ */
+ fun close(): Unit = closeable.cancel()
+
+ companion object {
+ private const val AWAIT_TIMEOUT_MS = 10L
+ }
+}
diff --git a/orbit-2-test/src/main/java/com/babylon/orbit2/TestStreamObserver.kt b/orbit-2-test/src/main/java/com/babylon/orbit2/TestStreamObserver.kt
index 5b70c6ed..7bce7073 100644
--- a/orbit-2-test/src/main/java/com/babylon/orbit2/TestStreamObserver.kt
+++ b/orbit-2-test/src/main/java/com/babylon/orbit2/TestStreamObserver.kt
@@ -24,6 +24,7 @@ import java.io.Closeable
*
* @param stream The stream to observe.
*/
+@Suppress("DEPRECATION")
class TestStreamObserver(stream: Stream) {
private val _values = mutableListOf()
private val closeable: Closeable
diff --git a/orbit-2-test/src/test/java/com/babylon/orbit2/OrbitTestingTest.kt b/orbit-2-test/src/test/java/com/babylon/orbit2/OrbitTestingTest.kt
index 6b9d82ab..fc6819fc 100644
--- a/orbit-2-test/src/test/java/com/babylon/orbit2/OrbitTestingTest.kt
+++ b/orbit-2-test/src/test/java/com/babylon/orbit2/OrbitTestingTest.kt
@@ -31,7 +31,7 @@ import org.junit.jupiter.params.provider.EnumSource
class OrbitTestingTest {
companion object {
- const val TIMEOUT = 500L
+ const val TIMEOUT = 1000L
}
val fixture = kotlinFixture()
@@ -81,6 +81,14 @@ class OrbitTestingTest {
testSubject.something(action)
testSubject.something(action2)
+ // Await two states before checking
+ testSubject.assert(timeoutMillis = TIMEOUT) {
+ states(
+ { copy(count = action) },
+ { copy(count = action2) }
+ )
+ }
+
val throwable = assertThrows {
testSubject.assert(timeoutMillis = TIMEOUT) {
states(
@@ -164,6 +172,7 @@ class OrbitTestingTest {
fun `fails if first emitted state does not match expected`(testCase: BlockingModeTests) {
val testSubject = StateTestMiddleware().test(
initialState = State(),
+ isolateFlow = false,
blocking = testCase.blocking
)
val action = fixture()
@@ -193,6 +202,7 @@ class OrbitTestingTest {
fun `fails if second emitted state does not match expected`(testCase: BlockingModeTests) {
val testSubject = StateTestMiddleware().test(
initialState = State(),
+ isolateFlow = false,
blocking = testCase.blocking
)
val action = fixture()
@@ -222,6 +232,7 @@ class OrbitTestingTest {
fun `fails if expected states are out of order`(testCase: BlockingModeTests) {
val testSubject = StateTestMiddleware().test(
initialState = State(),
+ isolateFlow = false,
blocking = testCase.blocking
)
val action = fixture()
diff --git a/orbit-2-viewmodel/src/main/java/com/babylon/orbit2/viewmodel/SavedStateContainerDecorator.kt b/orbit-2-viewmodel/src/main/java/com/babylon/orbit2/viewmodel/SavedStateContainerDecorator.kt
index a6af237d..329d402e 100644
--- a/orbit-2-viewmodel/src/main/java/com/babylon/orbit2/viewmodel/SavedStateContainerDecorator.kt
+++ b/orbit-2-viewmodel/src/main/java/com/babylon/orbit2/viewmodel/SavedStateContainerDecorator.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION")
+
package com.babylon.orbit2.viewmodel
import androidx.lifecycle.SavedStateHandle
@@ -21,8 +23,12 @@ import com.babylon.orbit2.Builder
import com.babylon.orbit2.Container
import com.babylon.orbit2.ContainerDecorator
import com.babylon.orbit2.Stream
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
import java.io.Closeable
+@Suppress("OverridingDeprecatedMember")
internal class SavedStateContainerDecorator(
override val actual: Container,
private val savedStateHandle: SavedStateHandle
@@ -30,6 +36,16 @@ internal class SavedStateContainerDecorator(
override val currentState: STATE
get() = actual.currentState
+ override val stateFlow: Flow
+ get() = flow {
+ actual.stateFlow.collect {
+ savedStateHandle[SAVED_STATE_KEY] = it
+ emit(it)
+ }
+ }
+ override val sideEffectFlow: Flow
+ get() = actual.sideEffectFlow
+
override val stateStream: Stream
get() = object : Stream {
override fun observe(lambda: (STATE) -> Unit): Closeable {
diff --git a/orbit-2-viewmodel/src/test/java/com/babylon/orbit2/viewmodel/ViewModelExtensionsKtTest.kt b/orbit-2-viewmodel/src/test/java/com/babylon/orbit2/viewmodel/ViewModelExtensionsKtTest.kt
index b47015a8..a9c84d40 100644
--- a/orbit-2-viewmodel/src/test/java/com/babylon/orbit2/viewmodel/ViewModelExtensionsKtTest.kt
+++ b/orbit-2-viewmodel/src/test/java/com/babylon/orbit2/viewmodel/ViewModelExtensionsKtTest.kt
@@ -27,6 +27,7 @@ import kotlinx.android.parcel.Parcelize
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
+@Suppress("DEPRECATION")
class ViewModelExtensionsKtTest {
private val fixture = kotlinFixture()
@@ -52,7 +53,7 @@ class ViewModelExtensionsKtTest {
}
@Test
- fun `Modified state is saved in the saved state handle`() {
+ fun `Modified state is saved in the saved state handle for stateStream`() {
val initialState = fixture()
val something = fixture()
val savedStateHandle = SavedStateHandle()
@@ -68,6 +69,23 @@ class ViewModelExtensionsKtTest {
)
}
+ @Test
+ fun `Modified state is saved in the saved state handle for stateFlow`() {
+ val initialState = fixture()
+ val something = fixture()
+ val savedStateHandle = SavedStateHandle()
+ val middleware = Middleware(savedStateHandle, initialState)
+ val testStateObserver = middleware.container.stateFlow.test()
+
+ middleware.something(something)
+
+ testStateObserver.awaitCount(2)
+
+ assertThat(savedStateHandle.get(SAVED_STATE_KEY)).isEqualTo(
+ TestState(something)
+ )
+ }
+
@Test
fun `When saved state is present calls onCreate with true`() {
val initialState = fixture()
diff --git a/samples/orbit-2-calculator/orbit-2-calculator_build.gradle.kts b/samples/orbit-2-calculator/orbit-2-calculator_build.gradle.kts
index 2c92ff2f..edd08739 100644
--- a/samples/orbit-2-calculator/orbit-2-calculator_build.gradle.kts
+++ b/samples/orbit-2-calculator/orbit-2-calculator_build.gradle.kts
@@ -21,10 +21,10 @@ plugins {
}
android {
- compileSdkVersion(29)
+ compileSdkVersion(30)
defaultConfig {
minSdkVersion(21)
- targetSdkVersion(29)
+ targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
applicationId = "com.babylon.orbit2.sample.calculator"
@@ -52,9 +52,11 @@ dependencies {
implementation(project(":orbit-2-livedata"))
implementation(project(":orbit-2-viewmodel"))
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
implementation("androidx.constraintlayout:constraintlayout:2.0.1")
implementation("com.google.android.material:material:1.2.1")
implementation("org.koin:koin-androidx-viewmodel:2.1.6")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
// Testing
testImplementation(project(":orbit-2-test"))
@@ -64,4 +66,5 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-params:5.6.2")
testImplementation("junit:junit:4.13")
testImplementation("com.appmattus.fixture:fixture:0.9.5")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9")
}
diff --git a/samples/orbit-2-calculator/src/main/java/com/babylon/orbit2/sample/calculator/CalculatorViewModel.kt b/samples/orbit-2-calculator/src/main/java/com/babylon/orbit2/sample/calculator/CalculatorViewModel.kt
index 08328eee..a8769713 100644
--- a/samples/orbit-2-calculator/src/main/java/com/babylon/orbit2/sample/calculator/CalculatorViewModel.kt
+++ b/samples/orbit-2-calculator/src/main/java/com/babylon/orbit2/sample/calculator/CalculatorViewModel.kt
@@ -20,8 +20,8 @@ import android.os.Parcelable
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
import com.babylon.orbit2.ContainerHost
-import com.babylon.orbit2.livedata.state
import com.babylon.orbit2.reduce
import com.babylon.orbit2.viewmodel.container
import kotlinx.android.parcel.Parcelize
@@ -36,7 +36,7 @@ class CalculatorViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
}
@Suppress("UNCHECKED_CAST")
- val state: LiveData = host.container.state as LiveData
+ val state: LiveData = host.container.stateFlow.asLiveData() as LiveData
fun clear() = host.orbit {
reduce {
diff --git a/samples/orbit-2-calculator/src/test/java/com/babylon/orbit2/sample/calculator/CalculatorViewModelTest.kt b/samples/orbit-2-calculator/src/test/java/com/babylon/orbit2/sample/calculator/CalculatorViewModelTest.kt
index 43c99acc..7c3a263f 100644
--- a/samples/orbit-2-calculator/src/test/java/com/babylon/orbit2/sample/calculator/CalculatorViewModelTest.kt
+++ b/samples/orbit-2-calculator/src/test/java/com/babylon/orbit2/sample/calculator/CalculatorViewModelTest.kt
@@ -22,7 +22,13 @@ import com.appmattus.kotlinfixture.kotlinFixture
import com.babylon.orbit2.sample.calculator.livedata.InstantTaskExecutorExtension
import com.babylon.orbit2.sample.calculator.livedata.MockLifecycleOwner
import com.babylon.orbit2.sample.calculator.livedata.test
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
import org.junit.Assert.assertEquals
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -34,16 +40,27 @@ import org.junit.jupiter.params.provider.ArgumentsProvider
import org.junit.jupiter.params.provider.ArgumentsSource
import java.util.stream.Stream
+@ExperimentalCoroutinesApi
@ExtendWith(InstantTaskExecutorExtension::class)
class CalculatorViewModelTest {
- private val viewModel = CalculatorViewModel(SavedStateHandle())
+ private val viewModel by lazy { CalculatorViewModel(SavedStateHandle()) }
private val mockLifecycleOwner = MockLifecycleOwner().also {
it.dispatchEvent(Lifecycle.Event.ON_CREATE)
it.dispatchEvent(Lifecycle.Event.ON_START)
}
+ @BeforeEach
+ fun beforeEach() {
+ Dispatchers.setMain(Dispatchers.Unconfined)
+ }
+
+ @AfterEach
+ fun afterEach() {
+ Dispatchers.resetMain()
+ }
+
/**
* Enter the whole number [value] into [this]
* @return Number of characters entered
diff --git a/samples/orbit-2-posts/README.md b/samples/orbit-2-posts/README.md
index 0e8d3ae3..634f6355 100644
--- a/samples/orbit-2-posts/README.md
+++ b/samples/orbit-2-posts/README.md
@@ -19,7 +19,7 @@ This sample implements a simple master-detail application using
[PostListFragment](src/main/java/com/babylon/orbit2/sample/posts/app/features/postlist/ui/PostListFragment.kt)
observes and sends to the `NavController`.
-- The state is accessed in the fragments through `LiveData`.
+- The state is accessed in the fragments through `Flow`.
- [PostListViewModel](src/main/java/com/babylon/orbit2/sample/posts/app/features/postlist/viewmodel/PostListViewModel.kt)
and
diff --git a/samples/orbit-2-posts/orbit-2-posts_build.gradle.kts b/samples/orbit-2-posts/orbit-2-posts_build.gradle.kts
index d78135f7..9b17dad2 100644
--- a/samples/orbit-2-posts/orbit-2-posts_build.gradle.kts
+++ b/samples/orbit-2-posts/orbit-2-posts_build.gradle.kts
@@ -27,10 +27,10 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
- compileSdkVersion(29)
+ compileSdkVersion(30)
defaultConfig {
minSdkVersion(21)
- targetSdkVersion(29)
+ targetSdkVersion(30)
applicationId = "com.babylon.orbit2.sample.posts"
versionCode = 1
versionName = "1.0"
@@ -46,7 +46,6 @@ dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(project(":orbit-2-core"))
implementation(project(":orbit-2-coroutines"))
- implementation(project(":orbit-2-livedata"))
implementation(project(":orbit-2-viewmodel"))
// UI
diff --git a/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postdetails/ui/PostDetailsFragment.kt b/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postdetails/ui/PostDetailsFragment.kt
index 60dc1eae..92a5a3dd 100644
--- a/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postdetails/ui/PostDetailsFragment.kt
+++ b/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postdetails/ui/PostDetailsFragment.kt
@@ -25,10 +25,9 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
-import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
-import com.babylon.orbit2.livedata.state
import com.babylon.orbit2.sample.posts.R
import com.babylon.orbit2.sample.posts.app.common.SeparatorDecoration
import com.babylon.orbit2.sample.posts.app.features.postdetails.viewmodel.PostDetailState
@@ -40,6 +39,7 @@ import com.bumptech.glide.request.transition.Transition
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import kotlinx.android.synthetic.main.post_details_fragment.*
+import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.stateViewModel
import org.koin.core.parameter.parametersOf
@@ -74,7 +74,9 @@ class PostDetailsFragment : Fragment() {
post_comments_list.adapter = adapter
- viewModel.container.state.observe(viewLifecycleOwner, Observer { render(it) })
+ lifecycleScope.launchWhenCreated {
+ viewModel.container.stateFlow.collect { render(it) }
+ }
}
private fun render(state: PostDetailState) {
diff --git a/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postlist/ui/PostListFragment.kt b/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postlist/ui/PostListFragment.kt
index b4802e37..0ae34916 100644
--- a/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postlist/ui/PostListFragment.kt
+++ b/samples/orbit-2-posts/src/main/java/com/babylon/orbit2/sample/posts/app/features/postlist/ui/PostListFragment.kt
@@ -22,18 +22,19 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
-import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
-import com.babylon.orbit2.livedata.sideEffect
-import com.babylon.orbit2.livedata.state
import com.babylon.orbit2.sample.posts.R
+import com.babylon.orbit2.sample.posts.app.common.NavigationEvent
import com.babylon.orbit2.sample.posts.app.common.SeparatorDecoration
import com.babylon.orbit2.sample.posts.app.features.postlist.viewmodel.OpenPostNavigationEvent
+import com.babylon.orbit2.sample.posts.app.features.postlist.viewmodel.PostListState
import com.babylon.orbit2.sample.posts.app.features.postlist.viewmodel.PostListViewModel
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import kotlinx.android.synthetic.main.post_list_fragment.*
+import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.stateViewModel
class PostListFragment : Fragment() {
@@ -45,15 +46,6 @@ class PostListFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- viewModel.container.sideEffect.observe(
- viewLifecycleOwner,
- Observer {
- when (it) {
- is OpenPostNavigationEvent ->
- findNavController().navigate(PostListFragmentDirections.actionListFragmentToDetailFragment(it.post))
- }
- }
- )
return inflater.inflate(R.layout.post_list_fragment, container, false)
}
@@ -73,11 +65,33 @@ class PostListFragment : Fragment() {
content.adapter = adapter
- viewModel.container.state.observe(
- viewLifecycleOwner,
- Observer {
- adapter.update(it.overviews.map { PostListItem(it, viewModel) })
+ lifecycleScope.launchWhenCreated {
+ viewModel.container.stateFlow.collect {
+ reduce(adapter, it)
+ }
+ }
+ lifecycleScope.launchWhenCreated {
+ viewModel.container.sideEffectFlow.collect {
+ sideEffect(it)
}
- )
+ }
+ }
+
+ private fun sideEffect(it: NavigationEvent) {
+ when (it) {
+ is OpenPostNavigationEvent ->
+ findNavController().navigate(
+ PostListFragmentDirections.actionListFragmentToDetailFragment(
+ it.post
+ )
+ )
+ }
+ }
+
+ private fun reduce(
+ adapter: GroupAdapter,
+ it: PostListState
+ ) {
+ adapter.update(it.overviews.map { PostListItem(it, viewModel) })
}
}
diff --git a/samples/orbit-2-stocklist/orbit-2-stocklist_build.gradle.kts b/samples/orbit-2-stocklist/orbit-2-stocklist_build.gradle.kts
index d59cef77..723cb6e7 100644
--- a/samples/orbit-2-stocklist/orbit-2-stocklist_build.gradle.kts
+++ b/samples/orbit-2-stocklist/orbit-2-stocklist_build.gradle.kts
@@ -22,10 +22,10 @@ plugins {
}
android {
- compileSdkVersion(29)
+ compileSdkVersion(30)
defaultConfig {
minSdkVersion(21)
- targetSdkVersion(29)
+ targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
applicationId = "com.babylon.orbit2.sample.stocklist"
@@ -80,6 +80,7 @@ dependencies {
implementation("com.xwray:groupie-viewbinding:2.8.1")
implementation("org.koin:koin-androidx-viewmodel:2.1.6")
implementation("androidx.lifecycle:lifecycle-common-java8:2.2.0")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Versions.desugar}")
}
diff --git a/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/detail/ui/DetailFragment.kt b/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/detail/ui/DetailFragment.kt
index 7b2d3713..03b95b0d 100644
--- a/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/detail/ui/DetailFragment.kt
+++ b/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/detail/ui/DetailFragment.kt
@@ -22,14 +22,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
-import androidx.lifecycle.Observer
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
-import com.babylon.orbit2.livedata.state
import com.babylon.orbit2.sample.stocklist.R
import com.babylon.orbit2.sample.stocklist.databinding.DetailFragmentBinding
import com.babylon.orbit2.sample.stocklist.detail.business.DetailViewModel
import com.babylon.orbit2.sample.stocklist.list.ui.JobHolder
import com.babylon.orbit2.sample.stocklist.list.ui.animateChange
+import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.stateViewModel
import org.koin.core.parameter.parametersOf
@@ -55,18 +56,17 @@ class DetailFragment : Fragment() {
super.onActivityCreated(savedInstanceState)
binding.apply {
- state = detailViewModel.container.state
+ state = detailViewModel.container.stateFlow.asLiveData()
lifecycleOwner = this@DetailFragment
}
- detailViewModel.container.state.observe(
- viewLifecycleOwner,
- Observer {
+ lifecycleScope.launchWhenCreated {
+ detailViewModel.container.stateFlow.collect {
it.stock?.let { stock ->
animateChange(binding.bid, binding.bidTick, stock.bid, bidRef)
animateChange(binding.ask, binding.askTick, stock.ask, askRef)
}
}
- )
+ }
}
}
diff --git a/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/list/ui/ListFragment.kt b/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/list/ui/ListFragment.kt
index c63254fb..7cbcb3ab 100644
--- a/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/list/ui/ListFragment.kt
+++ b/samples/orbit-2-stocklist/src/main/java/com/babylon/orbit2/sample/stocklist/list/ui/ListFragment.kt
@@ -22,18 +22,17 @@ import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
-import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
-import com.babylon.orbit2.livedata.sideEffect
-import com.babylon.orbit2.livedata.state
import com.babylon.orbit2.sample.stocklist.R
import com.babylon.orbit2.sample.stocklist.databinding.ListFragmentBinding
import com.babylon.orbit2.sample.stocklist.list.business.ListSideEffect
import com.babylon.orbit2.sample.stocklist.list.business.ListViewModel
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
+import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.stateViewModel
class ListFragment : Fragment() {
@@ -63,25 +62,22 @@ class ListFragment : Fragment() {
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
}
- listViewModel.container.state.observe(
- viewLifecycleOwner,
- Observer {
+ lifecycleScope.launchWhenCreated {
+ listViewModel.container.stateFlow.collect {
val items = it.stocks.map { stock ->
StockItem(stock, listViewModel)
}
groupAdapter.update(items)
}
- )
-
- listViewModel.container.sideEffect.observe(
- viewLifecycleOwner,
- Observer {
+ }
+ lifecycleScope.launchWhenCreated {
+ listViewModel.container.sideEffectFlow.collect {
when (it) {
is ListSideEffect.NavigateToDetail ->
findNavController().navigate(ListFragmentDirections.actionListFragmentToDetailFragment(it.itemName))
}
}
- )
+ }
}
}