Chaining and mapping kotlin flow - android

I'm trying to get a result from a flow, that retrieves a list from a room database, and then trying to map the list with another flow inside from another database operation, but I don't know if it is possible and if it is, how to make it, at this time I'm trying to make something like this
fun retrieveOperationsWithDues(client: Long): Flow<List<ItemOperationWithDues>> {
return database.operationsDao.getOperationCliente(client)
.flatMapMerge {
flow<List<ItemOperationWithDues>> {
it.map { itemOperation ->
database.duesDao.cuotasFromOperation(client, itemOperation.id).collectLatest { listDues ->
itemOperation.toItemOperationWithDues(listDues)
}
}
}
}
}
but looks like is not retrieving anything from the collect. Thanks in advice for any help

I think you don't need to use flow builder in flatMapMerge block. For each itemOperation you can call the cuotasFromOperatio() function from the Dao, which returns Flow and use combine() to combine retrieved flows:
fun retrieveOperationsWithDues(client: Long): Flow<List<ItemOperationWithDues>> {
return database.operationsDao.getOperationCliente(client)
.flatMapMerge {
val flows = it.map { itemOperation ->
database.duesDao.cuotasFromOperation(client, itemOperation.id).map { listDues ->
itemOperation.toItemOperationWithDues(listDues)
}
}
combine(flows) { flowArray -> flowArray.toList() }
}
}

Related

How to create a list from scanned BLE results

as invers to the question asked here How to convert Flow<List<Object>> to Flow<Object> I want to convert my Flow<Object> to Flow<List<Object>>.
At least I think I want that, so I try to explain what I want to achieve and give some background. I am working on an Android application that uses bluetooth to scan and connect to BLE devices. I'm fairly new to the Android platform and kotlin so I haven't quite grasped all the details despite all the many things I've already learnt.
My repository has a method which returns a Flow of ScanResults from the bluetooth adapter:
fun bluetoothScan(): Flow<ScanResult> {
return bluetoothStack.bluetoothScan()
}
My ViewModel consumes that function, maps the data to my BleScanResult and returns it as LiveData.
val scanResults: LiveData<BleScanResult> =
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.asLiveData()
In my activity I want to observer on that data and display it in a RecyclerView:
val adapter = ScanResultListAdapter()
binding.rcBleScanResults.adapter = adapter
viewModel.scanResults.observe(this) { result ->
//result.let { adapter.submitList(it) }
}
The problem is that scanResults is from type Flow<BleScanResult> and not Flow<List<BleScanResult>>, so the call to adapter.submitList(it) throws an error as it is expected to be a list.
So, how do I convert Flow to Flow<List> (with additional filtering of duplicates)? Or is there something I miss about the conception of Flow/LiveData?
You can try to use a MutableList and fill it with the data you get form a Flow, something like the following:
val results: MutableList<BleScanResult> = mutableListOf()
val scanResults: LiveData<List<BleScanResult>> =
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map {
results.apply {
add(BleScanResult(it.device.name, it.device.address))
}
}
} else {
emptyFlow()
}
}.asLiveData()
You can also use a MutableSet instead of MutableList if you want to have a unique list of items (assuming BleScanResult is a data class).
You could use the liveData builder to collect the Flow's values into a MutableList.
Here I copy the MutableList using toList() before emitting it since RecyclerView Adapters don't play well with mutable data sources.
val scanResults: LiveData<List<BleScanResult>> = liveData {
val cumulativeResults = mutableListOf<BleScanResult>()
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.collect {
cumulativeResults += it
emit(cumulativeResults.toList())
}
}
If you want to avoid duplicate entries and reordering of entries, you can use a set like this:
val scanResults: LiveData<List<BleScanResult>> = liveData {
val cumulativeResults = mutableSetOf<BleScanResult>()
scanEnabled.flatMapLatest { doScan ->
if (doScan) {
repository.bluetoothScan().map { BleScanResult(it.device.name, it.device.address) }
} else {
emptyFlow()
}
}.collect {
if (it !in cumulativeResults) {
cumulativeResults += it
emit(cumulativeResults.toList())
}
}
}

How to call a suspend function within a apply expression

I want to call a suspend function within an apply { } block.
I have a:
private suspend fun retrieve(accountAction: AccountAction): Any
suspend fun login() {
accountEvent.apply {
retrieve(it)
}
I tried to surround it with suspend { retrieve(it) } runblocking { retrieve(it) } but it seems that even if it’s not generating an error (Suspension functions can be called only within coroutine body) the code is not getting inside the retrieve function, but just passes through it and that’s why my unit tests fails.
FYI: this is a class, not an activity or a fragment.
Edit:
This is the actual code (from comment):
override suspend fun login(webView: WebView) = trackingId()
.flatMap { id -> AccountAction(client, id, WeakReference(webView), upgradeAccount) }
.map {
it.apply {
upgradeWebViewProgress(webView)
suspend { retrieve(it) }
}
}
.flatMap { updateAuth(it) }
You can use the Flow-API when you want to do asynchronous (suspend) operations on a list of elements like this. You can read about that API here: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
Probably the simplest way to get your example working is by converting your list to a Flow, performing the suspending operations, then converting back to a List. Like this:
override suspend fun login(webView: WebView) = trackingId()
.flatMap { id -> AccountAction(client, id, WeakReference(webView), upgradeAccount) }
.asFlow()
.map {
it.apply {
upgradeWebViewProgress(webView)
retrieve(it)
}
}
.toList()
.flatMap { updateAuth(it) }
Note that this might not be the most efficient, because it will perform the retrieve-operations sequentially. You can use other operators on Flow to perform the operations in parallel for example.
Edited:
This shows an alternative without using map as it is not really required in my opionion for this example (except you really wanna chain all your calls)
suspend fun login(webView: WebView) {
val result = trackingId().flatMap { id -> AccountAction(client, id, WeakReference(webView), upgradeAccount) }
upgradeWebViewProgress(webView)
return retrieve(result).flatMap { updateAuth(it) } }

Kotlin Coroutines - Suspend function returning a Flow runs forever

I am making a network repository that supports multiple data retrieval configs, therefore I want to separate those configs' logic into functions.
However, I have a config that fetches the data continuously at specified intervals. Everything is fine when I emit those values to the original Flow. But when I take the logic into another function and return another Flow through it, it stops caring about its coroutine scope. Even after the scope's cancelation, it keeps on fetching the data.
TLDR: Suspend function returning a flow runs forever when currentCoroutineContext is used to control its loop's termination.
What am I doing wrong here?
Here's the simplified version of my code:
Fragment calling the viewmodels function that basically calls the getData()
lifecycleScope.launch {
viewModel.getLatestDataList()
}
Repository
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
//It worked fine when fetchContinuously was ingrained to here and emitted directly to the current flow
//And now it keeps on running eternally
fetchContinuously().collect { updatedList ->
emit(updatedList)
}
}
}
}
}
//Note logic of this function is greatly reduced to keep the focus on the problem
private suspend fun fetchContinuously(): Flow<List<Data>>
{
return flow {
while (currentCoroutineContext().isActive)
{
val updatedList = fetchDataListOverNetwork().await()
if (updatedList != null)
{
emit(updatedList)
}
delay(refreshIntervalInMs)
}
Timber.i("Context is no longer active - terminating the continuous-fetch coroutine")
}
}
private suspend fun fetchDataListOverNetwork(): Deferred<List<Data>?> =
withContext(Dispatchers.IO) {
return#withContext async {
var list: List<Data>? = null
try
{
val response = apiService.getDataList().execute()
if (response.isSuccessful && response.body() != null)
{
list = response.body()!!.list
}
else
{
Timber.w("Failed to fetch data from the network database. Error body: ${response.errorBody()}, Response body: ${response.body()}")
}
}
catch (e: Exception)
{
Timber.w("Exception while trying to fetch data from the network database. Stacktrace: ${e.printStackTrace()}")
}
finally
{
return#async list
}
list //IDE is not smart enough to realize we are already returning no matter what inside of the finally block; therefore, this needs to stay here
}
}
I am not sure whether this is a solution to your problem, but you do not need to have a suspending function that returns a Flow. The lambda you are passing is a suspending function itself:
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T> (source)
Here is an example of a flow that repeats a (GraphQl) query (simplified - without type parameters) I am using:
override fun query(query: Query,
updateIntervalMillis: Long): Flow<Result<T>> {
return flow {
// this ensures at least one query
val result: Result<T> = execute(query)
emit(result)
while (coroutineContext[Job]?.isActive == true && updateIntervalMillis > 0) {
delay(updateIntervalMillis)
val otherResult: Result<T> = execute(query)
emit(otherResult)
}
}
}
I'm not that good at Flow but I think the problem is that you are delaying only the getData() flow instead of delaying both of them.
Try adding this:
suspend fun getData(config: MyConfig): Flow<List<Data>>
{
return flow {
when (config)
{
CONTINUOUS ->
{
fetchContinuously().collect { updatedList ->
emit(updatedList)
delay(refreshIntervalInMs)
}
}
}
}
}
Take note of the delay(refreshIntervalInMs).

Coroutines Flow with Dao and generic Result class

I have a Dao class which returns List of Car objects as flow.
#Query("SELECT * FROM Car")
fun getAllCars(): Flow<List<Car>>
In my repository, I can use this Dao as follows
fun getAllCars(): Flow<List<Car>>
//Implementation
override fun getAllCars() = carDao.getAllCars()
I can observe this flow in view model and everything works and life was fine.
Now, after reading the post on Developer Android site about
A generic class that contains data and status about loading this data.
I got inspired, so I read one more post here which discuss about having Result class.
So, I have done some changes to repository and I am not able to solve them.
Error:
suspend fun getSomeData(): Flow<Result<List<Car>>> {
carDao.getAllCars().collect {
if (it.isNotEmpty()) {
return flowOf(Result.Success(it)) //<-- Here I am getting error from IDE
}
else {
val throwable = Throwable()
return flowOf(Result.Failure<List<Car>>(throwable)) //<-- Here I am getting error from IDE
}
}
}
The error is Return is not allowed here and Change to 'return#Collect'
What I want to achieve is:
// At view model side
viewmodelScope.launch {
repo.getSomeData().collect {
if (it == Result.Success) {
//Show data
}
else {
//Show empty screen
}
}
}
Is my approach of implementation of Result is wrong? I am not able to figure out what is wrong. Why I can't just return Flow from a flow
If you want to use Result, you should should return Result < YourClass>. It will look like that :
suspend fun getSomeData(): Result<Flow<List<Car>>> {
return carDao.getAllCars().collect {
if (it.isNotEmpty()) {
Result.Success(flowOf(it))
} else {
Result.Failure(Throwable()))
}
}
}
This is what your function should look like. Note there's no need for it to be a suspend fun.
fun getSomeData(): Flow<Result<List<Car>>> = flow {
carDao.getAllCars().collect {
if (it.isNotEmpty()) {
emit(Result.Success(it))
}
else {
emit(Result.Failure<List<Car>>(Throwable()))
}
}
}
But what it does is nothing more than adding a mapping step, which you can generalize.
fun <T> Flow<List<T>>.toResultFlow(): Flow<Result<List<T>>> = this.map {
if (it.isNotEmpty()) Result.Success(it)
else Result.Failure(Throwable())
}

How do you call a suspend function inside a SAM?

I'm trying to create a Flow that needs to emit values from a callback but I can't call the emit function since the SAM is a normal function
Here's the class with the SAM from a library that I can't really modify it the way I need it to be.
class ValueClass {
fun registerListener(listener: Listener) {
...
}
interface Listener {
fun onNewValue(): String
}
}
And here's my take on creating the Flow object
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
emit(value) // Suspension functions can only be called on coroutine body
}
}
}
I guess it would've been simple if I could change the ValueClass but in this case, I can't. I've been wrapping my head around this and trying to look for implementations.
At least from what I know so far, one solution would be to use GlobalScope like this
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
GlobalScope.launch {
emit(value)
}
}
}
}
Now, this works but I don't want to use GlobalScope since I'll be using viewModelScope to tie it to my app's lifecycle.
Is there any way to work around this?
Thanks in advance. Any help would be greatly appreciated!
You can use callbackFlow to create a Flow from the callback. It will look something like:
fun listenToValue(): Flow<String> = callbackFlow {
valueClass.registerListener { value ->
trySend(value)
channel.close() // close channel if no more values are expected
}
awaitClose { /*unregister listener*/ }
}
Or if only one value is expected from the callback, you can use suspendCoroutine or suspendCancellableCoroutine. It this case listenToValue() function must be suspend and later called from a coroutine(e.g. someScope.launch):
suspend fun listenToValue(): String = suspendCoroutine { continuation ->
valueClass.registerListener { value ->
continuation.resumeWith(value)
}
}

Categories

Resources