mikolaj-jalocha
mikolaj-jalocha

Reputation: 728

Filtering collection inside onEach in Flow doesn't work

In particular use case I'm making a repository call to obtain data in form of Flow.

It's type is:

Flow<Resource<List<Location>>>

Where:

Each location has it's own property like type. When user switch to section where type's Hotel, use case method is triggered, api call is made and I'm filtering list so that it contains only desirable items.

However the problem is filtering mechanims which doesn't work.

return repository.getLocations()
        .onEach { result ->
            if (result.data != null) {
                when (locationType) {
                    is LocationType.All -> result.data
                    is LocationType.Hotel -> result.data.filter { it.type == "Hotel" }
                    is LocationType.Explore -> result.data.filter { it.type == "Explore" }
                    is LocationType.Active -> result.data.filter { it.type == "Active" }
                    is LocationType.Restaurant -> result.data.filter { it.type == "Restaurant" }
                }   
            }
        }

Final list isn't changed despite filtering with use ofonEach

UPDATE

The return type of repository call is:

Flow<Resource<List<Location>>>

SOLUTION

Finally I've come up with the solution. In my use case I'm collecting the flow and inside collect lambda, I've put code responsible for filtering. When everything is done I'm just emitting data further ;)

operator fun invoke(
    locationType: LocationType
) = flow {
     repository.getLocations().collect { result ->
        if (result.data != null) {
            when (locationType) {
                is LocationType.All -> result.data
                is LocationType.Hotel -> result.data.filter { it.type == "Hotel" }
                is LocationType.Explore -> result.data.filter { it.type == "Explore" }
                is LocationType.Active -> result.data.filter { it.type == "Active" }
                is LocationType.Restaurant -> result.data.filter { it.type == "Restaurant" }.also { res ->
                when (result) {
                    is Resource.Success -> {
                        emit(Resource.Success(data = res))   }
                    is Resource.Loading -> {
                        emit(Resource.Loading(data = res))
                    }
                    is Resource.Error -> {
                        emit(Resource.Error(data = res, message = result.message ?: ""))
                    }
                }
            }

Upvotes: 0

Views: 1236

Answers (2)

SpiritCrusher
SpiritCrusher

Reputation: 21053

If you want to filter the result you should filter it directly . the onEach is not needed here. You can do it this way .

val result = repository.getLocations()
    return if(result.data!=null){
        result.data.filter { item ->
            when (locationType) {
                is LocationType.Hotel -> item.type == "Hotel"
                is LocationType.Explore -> item.type == "Explore"
                is LocationType.Active -> item.type == "Active"
                is LocationType.Restaurant ->item.type == "Restaurant"
                else -> true
            }
        }
    }else{
        emptyList()
    }

this is just for explanation you can modify and make it more kotlinify. LocationType is a constant here so you can directly do something like this you do not need a when here.

val result = repository.getLocations()
    return if(result.data!=null){
        result.data.filter { item ->
                item.type == locationType
        }
    }else{
        emptyList()
    }

To return a Flow you can return something like below.

result.map { it.data.filter { item -> item.type == locationType } }

Upvotes: 1

Tenfour04
Tenfour04

Reputation: 93739

filter doesn't filter a list in place. It returns a filtered copy of the list.

It also doesn't make sense to use onEach on your Flow. This is only creating a new Flow that will perform the onEach action on each emitted item when it gets collected. Since you are returning from this function, maybe you just need the first item from the Flow, in which case you should use the first() function on it and then work with the returned value.

You need to create your filtered copy one time. Then you can put that filtered copy back into a new Success instance to return.

val result repository.getLocations().first()
if (result !is Success<List<Location>>) {
    return result
}
val filteredData = result.data?.filter {
    it.type == when (locationType) {
        is LocationType.All -> it.type
        is LocationType.Hotel -> "Hotel"
        is LocationType.Explore -> "Explore"
        is LocationType.Active -> "Active"
        is LocationType.Restaurant -> "Restaurant"
    }
}
return Success(data = filteredData)

Side note, you are missing the point of using a sealed class. Since you are putting all the possible properties in the parent class, there's no point in making it sealed and giving it children--it could just have one more property that indicates it represents loading, success, or error. Now you have to deal with nullable data and error message even if you've checked the child type. The whole point of using a sealed class vs. a single class would be to avoid having to make those nullable. Your parent class should have no properties defined. The Loading class doesn't need properties and can therefore be an object. Your Success class can have a single non-nullable data property, and the Error class can have a single non-nullable message property. The Success and Error classes can be data classes so they are more easily compared. It should look like this:

sealed class Resource<T> {
    object Loading<T>: Resource<T>()
    data class Success<T>(val data: T): Resource<T>()
    data class Error<T>(message: String): Resource<T>()
}

Upvotes: 1

Related Questions