Bencri
Bencri

Reputation: 1283

Build a list from kotlin flow

I'm trying to build an application that use kotlin flows from the data layer up to the view but struggle so much with simple problems like this one.

I'm collecting integer values from a StateFlow coming from my data layer, and would like to build a list out of it before I share it to me viewmodel and view throught another StateFlow.

Sample code:

class MyDataSource {
    val inputFlow: StateFlow<Int> = TODO()
}

class MyService(
    dataSource: MyDataSource
) {
    private val intList: MutableList<Int> = ArrayList()

    private val _intFlow = MutableStateFlow<List<Int>>(listOf())
    val intFlow: StateFlow<List<Int>> = _intFlow

    init {
        GlobalScope.launch(Dispatchers.IO) {
            dataSource.inputFlow.collect {
                if (!intList.contains(it))
                    intList.add(it)
                
                _intFlow.emit(ArrayList(intList))
            }
        }
    }
}

Isn't there a better way to do this? Like a flow operator I missed? It seems especially ugly since I need to build a new ArrayList every time because other wise even if the content of the List changed the flow won't send the new value as it is still the same List.

Upvotes: 1

Views: 9946

Answers (2)

Sam
Sam

Reputation: 9944

Does scan do what you need?

From the docs:

Folds the given flow with operation, emitting every intermediate result, including initial value. [...] For example:

flowOf(1, 2, 3).scan(emptyList<Int>()) { acc, value -> acc + value }.toList() will produce [], [1], [1, 2], [1, 2, 3]].

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/scan.html

Upvotes: 0

Tenfour04
Tenfour04

Reputation: 93521

There is some cleanup you can do, but I think the arrangement of flowing an ever growing list is too specific for there to be any all-in-one solution in the core library.

  1. You should avoid using GlobalScope. This service class should more properly have its own scope so you can manage lifecycle if need be.

  2. Your backing list should be a set since you are in effect using it as a poor-performance set by checking if the objects are in it before adding to it.

  3. It would be cleaner to use chain operators and stateIn rather than manipulating a MutableStateFlow and having to use a backing property.

class MyService(
    dataSource: MyDataSource
) {
    private val intSet = mutableSetOf<Int>()
    private val scope = CoroutineScope(Dispatchers.Default)

    val intFlow = dataSource.inputFlow
        .map {
            intSet += it
            intSet.toList()
        }.stateIn(scope, SharingStarted.Eagerly, emptyList())
}

Edit: Here's an untested attempt at avoiding set-to-list copies and new allocations on each iteration:

class MyService(
    dataSource: MyDataSource
) {
    private val intSet = mutableSetOf<Int>()
    private var currentList = mutableListOf<Int>()
    private var previousList = mutableListOf<Int>()
    private var lastEmittedValue: Int? = null
    private val scope = CoroutineScope(Dispatchers.Default)

    val intFlow = dataSource.inputFlow
        .mapNotNull { newValue ->
            if (!intSet.add(newValue)) 
                return@mapNotNull null
            lastEmittedValue?.let {
                previousList += it
            } // both backing lists now identical
            lastEmittedValue = newValue
            previousList = currentList.also { currentList = previousList }
            // previousList is the one missing lastEmittedValue on next iteration:
            currentList += newValue
            currentList
        }.stateIn(scope, SharingStarted.Eagerly, emptyList())
}

Upvotes: 1

Related Questions