Does it matter how to get Flow from anywhere(repository, etc.)? - kotlin

I am getting my flow list like that:
val list = repository.someFlowList()
Sometimes I do this like that:
fun list() = repository.someFlowList()
In the Google Codelab it's used like that:
val list: Flow<List<Something>>
get() = repository.someFlowList()
I know what properties, getters, setters, functions are. But I want to know only one thing: is there any difference in terms of efficency, performance, etc? If it matters, I use that flow as livedata(just using asLiveData() method) in activity.

TL;DR: You should prefer a field-backed property over a computed property, but the outcome is the same. If it's a hot flow, in this case you should use a field-backed property to avoid unexpected behavior.
The Flow API is sort of declarative, meaning that when you create a Flow you are just defining what it does, this is why it's said to be a cold flow. The computation you define only runs when the flow is collected. This means that you can call collect on the same instance as many times as you need, the computation it defines will run from scratch each time. The only underlying impact of creating a new Flow instance each time as a computed property, or as result of a function, is that you allocate more instances of the same Flow definition.
Cold Flow
Check this trivial example:
val flow = flow {
emit(0)
emit(1)
}
runBlocking {
val f1 = flow
f1.collect {
println("Flow ID: ${f1.hashCode()} - emits $it")
}
val f2 = flow
f2.collect {
println("Flow ID: ${f2.hashCode()} - emits $it")
}
}
This will print:
Flow ID: 608188624 - emits 0
Flow ID: 608188624 - emits 1
Flow ID: 608188624 - emits 0
Flow ID: 608188624 - emits 1
You see that the same Flow instance, when collected, will run the Flow emission, each time you collect it.
If you change the assignment with a getter (val flow get() = flow {...}), the output is:
Flow ID: 608188624 - emits 0
Flow ID: 608188624 - emits 1
Flow ID: 511833308 - emits 0
Flow ID: 511833308 - emits 1
See that the outcome is the same, the difference is that now you have 2 Flow instances.
Hot Flow
When the Flow is hot, that is, it has values even before a collector starts collecting, then the story is different. A StateFlow is a typical hot Flow. Check this out:
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
val flow = MutableStateFlow(0)
val job1 = scope.launch {
val f = flow
f.value = 1 // <-- note this
f.collect {
println("A: $it")
}
}
val job2 = scope.launch {
val f = flow
f.collect {
println("B: $it")
}
}
runBlocking {
job1.cancelAndJoin()
job2.cancelAndJoin()
}
The output is:
A: 1
B: 1
Both Flow collections receive the latest value of the single hot Flow instance.
If you change to val flow get() = MutableStateFlow(0) you get:
A: 1
B: 0
This time we create a different instance of the StateFlow, so the second collector misses the value change that we did before on the first Flow instance. This is a problem if the property is exposed as public, since the implementation of the property, either field-backed or computed, should not be relevant to the caller. Eventually this may end up creating an unexpected behaviour - a bug.

In Kotlin there is a concept of Backing Fields, these fields are only used when needed as part of a property to hold its value in memory.
Using a getter function get() = repository.someFlowList() the body is evaluated every time the property is accessed since there is no backing field assigned to it. While in case of val list = repository.someFlowList() value is evaluated during initialization and saved in a backing field. Kotlin documents on Getters & Setters explains it too.

Related

How to pass Observable emissions to MutableSharedFlow?

well, I have an Observable, I’ve used asFlow() to convert it but doesn’t emit.
I’m trying to migrate from Rx and Channels to Flow, so I have this function
override fun processIntents(intents: Observable<Intent>) {
intents.asFlow().shareTo(intentsFlow).launchIn(this)
}
shareTo() is an extension function which does onEach { receiver.emit(it) }, processIntents exists in a base ViewModel, and intentsFlow is a MutableSharedFlow.
fun <T> Flow<T>.shareTo(receiver: MutableSharedFlow<T>): Flow<T> {
return onEach { receiver.emit(it) }
}
I want to pass emissions coming from the intents Observable to intentsFlow, but it doesn’t work at all and the unit test keeps failing.
#Test(timeout = 4000)
fun `WHEN processIntent() with Rx subject or Observable emissions THEN intentsFlow should receive them`() {
return runBlocking {
val actual = mutableListOf<TestNumbersIntent>()
val intentSubject = PublishSubject.create<TestNumbersIntent>()
val viewModel = FlowViewModel<TestNumbersIntent, TestNumbersViewState>(
dispatcher = Dispatchers.Unconfined,
initialViewState = TestNumbersViewState()
)
viewModel.processIntents(intentSubject)
intentSubject.onNext(OneIntent)
intentSubject.onNext(TwoIntent)
intentSubject.onNext(ThreeIntent)
viewModel.intentsFlow.take(3).toList(actual)
assertEquals(3, actual.size)
assertEquals(OneIntent, actual[0])
assertEquals(TwoIntent, actual[1])
assertEquals(ThreeIntent, actual[2])
}
}
test timed out after 4000 milliseconds
org.junit.runners.model.TestTimedOutException: test timed out after
4000 milliseconds
This works
val ps = PublishSubject.create<Int>()
val mf = MutableSharedFlow<Int>()
val pf = ps.asFlow()
.onEach {
mf.emit(it)
}
launch {
pf.take(3).collect()
}
launch {
mf.take(3).collect {
println("$it") // Prints 1 2 3
}
}
launch {
yield() // Without this we suspend indefinitely
ps.onNext(1)
ps.onNext(2)
ps.onNext(3)
}
We need the take(3)s to make sure our program terminates, because MutableSharedFlow and PublishSubject -> Flow collect indefinitely.
We need the yield because we're working with a single thread and we need to give the other coroutines an opportunity to start working.
Take 2
This is much better. Doesn't use take, and cleans up after itself.
After emitting the last item, calling onComplete on the PublishSubject terminates MutableSharedFlow collection. This is a convenience, so that when this code runs it terminates completely. It is not a requirement. You can arrange your Job termination however you like.
Your code never terminating is not related to the emissions never being collected by the MutableSharedFlow. These are separate concerns. The first is due to the fact that neither a flow created from a PublishSubject, nor a MutableSharedFlow, terminates on its own. The PublishSubject flow will terminate when onComplete is called. The MutableSharedFlow will terminate when the coroutine (specifically, its Job) collecting it terminates.
The Flow constructed by PublishSubject.asFlow() drops any emissions if, at the time of the emission, collection of the Flow hasn't suspended, waiting for emissions. This introduces a race condition between being ready to collect and code that calls PublishSubject.onNext().
This, I believe, is the reason why flow collection isn't picking up the onNext emissions in your code.
It's why a yield is required right after we launch the coroutine that collects from psf.
val ps = PublishSubject.create<Int>()
val msf = MutableSharedFlow<Int>()
val psf = ps.asFlow()
.onEach {
msf.emit(it)
}
val j1 = launch {
psf.collect()
}
yield() // Use this to allow psf.collect to catch up
val j2 = launch {
msf.collect {
println("$it") // Prints 1 2 3 4
}
}
launch {
ps.onNext(1)
ps.onNext(2)
ps.onNext(3)
ps.onNext(4)
ps.onComplete()
}
j1.invokeOnCompletion { j2.cancel() }
j2.join()

KafkaConsumer: `seekToEnd()` does not make consumer consume from latest offset

I have the following code
class Consumer(val consumer: KafkaConsumer<String, ConsumerRecord<String>>) {
fun run() {
consumer.seekToEnd(emptyList())
val pollDuration = 30 // seconds
while (true) {
val records = consumer.poll(Duration.ofSeconds(pollDuration))
// perform record analysis and commitSync()
}
}
}
}
The topic which the consumer is subscribed to continously receives records. Occasionally, the consumer will crash due to the processing step. When the consumer then is restarted, I want it to consume from the latest offset on the topic (i.e. ignore records that were published to the topic while the consumer was down). I thought the seekToEnd() method would ensure that. However, it seems like the method has no effect at all. The consumer starts to consume from the offset from which it crashed.
What is the correct way to use seekToEnd()?
Edit: The consumer is created with the following configs
fun <T> buildConsumer(valueDeserializer: String): KafkaConsumer<String, T> {
val props = setupConfig(valueDeserializer)
Common.setupConsumerSecurityProtocol(props)
return createConsumer(props)
}
fun setupConfig(valueDeserializer: String): Properties {
// Configuration setup
val props = Properties()
props[ConsumerConfig.GROUP_ID_CONFIG] = config.applicationId
props[ConsumerConfig.CLIENT_ID_CONFIG] = config.kafka.clientId
props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = config.kafka.bootstrapServers
props[AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG] = config.kafka.schemaRegistryUrl
props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = config.kafka.stringDeserializer
props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = valueDeserializer
props[KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG] = "true"
props[ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG] = config.kafka.maxPollIntervalMs
props[ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG] = config.kafka.sessionTimeoutMs
props[ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG] = "false"
props[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = "false"
props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = "latest"
return props
}
fun <T> createConsumer(props: Properties): KafkaConsumer<String, T> {
val consumer = KafkaConsumer<String, T>(props)
consumer.subscribe(listOf(config.kafka.inputTopic))
return consumer
}
I found a solution!
I needed to add a dummy poll as a part of the consumer initialization process. Since several Kafka methods are evaluated lazily, it is necessary with a dummy poll to assign partitions to the consumer. Without the dummy poll, the consumer tries to seek to the end of partitions that are null. As a result, seekToEnd() has no effect.
It is important that the dummy poll duration is long enough for the partitions to get assigned. For instance with consumer.poll((Duration.ofSeconds(1)), the partitions did not get time to be assigned before the program moved on to the next method call (i.e. seekToEnd()).
Working code could look something like this
class Consumer(val consumer: KafkaConsumer<String, ConsumerRecord<String>>) {
fun run() {
// Initialization
val pollDuration = 30 // seconds
consumer.poll((Duration.ofSeconds(pollDuration)) // Dummy poll to get assigned partitions
// Seek to end and commit new offset
consumer.seekToEnd(emptyList())
consumer.commitSync()
while (true) {
val records = consumer.poll(Duration.ofSeconds(pollDuration))
// perform record analysis and commitSync()
}
}
}
}
The seekToEnd method requires the information on the actual partition (in Kafka terms TopicPartition) on which you plan to make your consumer read from the end.
I am not familiar with the Kotlin API, but checking the JavaDocs on the KafkaConsumer's method seekToEnd you will see, that it asks for a collection of TopicPartitions.
As you are currently using emptyList(), it will have no impact at all, just like you observed.

Kotlin coroutines: How to flatMapConcat 3 flows, if one of them has not started emitting yet?

I have 3 flows:
val flow1 = ... // emits values from the app start
val flow2 = ... // emits values from the app start
val flow3 = someBooleanFlow.flatMapLatest{ if(it) flowOf(42) else emptyFlow() } // emits when user registered
I need:
val state = flow1.flatMapConcat{ value1 ->
flow2.flatMapConcat{ value2 ->
flow3.flatMapConcat{ value3 -> when{ ... } // so value3 is not received if flow3 is empty,
// but I need its default value here to perform "when" logics
}
}
}
The question is how to take a default value in flatMapConcat when one of flows has not emitted?
The issue is also valid for combine
You can use onStart on a Flow to emit an initial value:
val flowWithInitialValue = sourceFlow.onStart { emit(initialValue) }
You could also use a StateFlow for this, and provide your initial value at construction time.
I'm guessing flow3 represents some kind of user ID for the logged in user.
In this case, I would not use an emptyFlow() to represent the non-logged case. You can probably use a nullable user ID or a sealed class as element of your flow, so you can emit null or UserLoginStatus.Anonymous as a first default value.

Flow - pause/resume flow

In RxJava there is the valve operator that allows to pause (and buffer) a flow and resumes the flow again (and also emit the buffered values as soon as it's resumed). It's part of the rx java extensions (https://github.com/akarnokd/RxJavaExtensions/blob/3.x/src/main/java/hu/akarnokd/rxjava3/operators/FlowableValve.java).
Is there something like this for kotlin flows?
My use case is that I want to observe a flow inside an activity and never lose an event (like I would do it with LiveData e.g. which stops observing data if the activity is paused). So while the activity is paused I want the flow to buffer observed values until the activity is resumed and emit them all as soon as the activity is resumed.
So while the activity is created (until it is destroyed) I want to observe the flow BUT I only want to emit values while the activity is active and buffer the values while it is not active (but still created) until it gets active again.
Is there something to solve this or has anyone ever written something to solve this?
A combination of Lifecycle.launchWhenX and a SharedFlow should do the trick. Here's a simple example using a flow that emits a number every second.
// In your ViewModel
class MainViewModel : ViewModel() {
val numbers = flow {
var counter = 0
while (true) {
emit(counter++)
delay(1_000L)
}
}
.shareIn(
scope = viewModelScope,
started = SharingStarted.Lazily
)
}
// In your Fragment.onViewCreated()
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.numbers
.collect { number ->
Log.d("asdf", "number: $number")
}
}
This works because Lifecycle.launchWhenStarted pauses the coroutine when the Lifecycle enters a stopped state, rather than cancels it. When your Lifecycle comes back to a started state after pausing, it'll collect everything that happened while in the stopped state.
I know it is ugly solution but it works fine for me:
fun main() {
val flow = MutableSharedFlow<String>(extraBufferCapacity = 50, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val isOpened = AtomicBoolean()
val startTime = System.currentTimeMillis()
GlobalScope.launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) {
flow
.transform { value ->
while (isOpened.get().not()) { }
emit(value)
}
.collect {
println("${System.currentTimeMillis() - startTime}: $it")
}
}
Thread.sleep(1000)
flow.tryEmit("First")
Thread.sleep(1000)
isOpened.set(true)
flow.tryEmit("Second")
isOpened.set(false)
Thread.sleep(1000)
isOpened.set(true)
flow.tryEmit("Third")
Thread.sleep(2000)
}
Result:
So you can set isOpened to false when your activity lifecycle paused and to true when resumed.
You can use lifecycleScope.launchWhenStarted
https://developer.android.com/kotlin/flow/stateflow-and-sharedflow#stateflow

When is a NoSuchElementException thrown for a call to flow.single()

Say I have an API like so:
interface Foo {
val barFlow: Flow<Bar>
}
And I consume it like so:
class FooConsumer(private val foo: Foo) {
init {
CoroutineScope(Dispatchers.IO).launch {
val bar = foo.barFlow.single()
println("Collected bar: $bar)
}
}
}
According to the docs for single a NoSuchElementException can be thrown if the flow is empty. However, this confuses me quite a lot, as a terminal operation on a flow will "await" elements of the flow to be emitted. So how will the call to single know that there were no elements in the flow? Maybe an element just hasn't been emitted yet?
I mean under the hood, the call to single is collecting the source flow before it does the check. Therefore at least 1 item must have been emitted before the check for null is carried out, so that null check should never succeed and a NoSuchElementException should never be thrown (for the case where the flow is of a non nullable type).
So will NoSuchElementException only be a possibility for flows of nullable types?
Here is the source code for single:
/**
* The terminal operator, that awaits for one and only one value to be published.
* Throws [NoSuchElementException] for empty flow and [IllegalStateException] for flow
* that contains more than one element.
*/
public suspend fun <T> Flow<T>.single(): T {
var result: Any? = NULL
collect { value ->
if (result !== NULL) error("Expected only one element")
result = value
}
if (result === NULL) throw NoSuchElementException("Expected at least one element")
#Suppress("UNCHECKED_CAST")
return result as T
}
NoSuchElementException is thrown when the Flow finishes its emission without emitting a single element. One case I can think of right now is when you need to turn a collection into a Flow source. If that collection is empty and you call single on that Flow you will get a NoSuchElementException.
This example may seem absurd but you get the point:
val emptyListFlow = emptyList<Int>().asFlow()
launch {
val data = emptyListFlow.single()
}
In my case, I made a list.first(), where the list was empty