hey all, curious if any of you might be able to he...
# coroutines
r
hey all, curious if any of you might be able to help me solve a StateFlow/Channel predicament. i've got a
StateFlow<State>
setup to track state changes of a screen in Android. these state changes are observed by the
View
. additionally, i use a
Channel<Intent>
to
offer
some
Intent
from the
View
to the
ViewModel
. the problem is that my code will suspend and it feels like the main thread is blocked until an
Intent
is fully processed by the
ViewModel
. i've added some of my code below to hopefully help with understanding the problem
Copy code
// ViewModel

    val intentChannel = Channel<Intent>(Channel.UNLIMITED)
    private val _state = MutableStateFlow<State>(Idle)
    val state: StateFlow<State>
        get() = _state

    init {
        viewModelScope.launch {
            handleIntents()
        }
    }

    // TODO: we seem to suspend the main thread every time we process an intent; NOT GOOD!
    private suspend fun handleIntents() {
        intentChannel.consumeAsFlow().collect {
            when (it) {
                is Init -> with(it) {
                    // do something on initialization
                }
                is Load -> with(it) {
                    _state.value = Loading(page == 0)
                    getBrandProducts(page = page)
                }
                is Sort -> with(it) {
                    _state.value = Sorting
                    getBrandProducts(page = 0)
                }
            }
        }
    }

    // brandRepository flows on a <http://Dispatcher.IO|Dispatcher.IO>
    private suspend fun getBrandProducts(page: Int) {
        brandRepository.getPromotedProducts(
            categorySlug = categorySlug,
            sortBy = currentSortBy,
            sortOrder = currentSortOrder,
            offset = page * requestLimit,
            limit = requestLimit
        )
            .map { result -> result.getOrNull()!! }
            .collect { result -> _state.value = Loaded(result) }
    }
⏸️ 1
☘️ 1
z
my code will suspend and block the main thread
“suspend” and “block” mean different things. Blocking the main thread is (generally) bad. Suspending a coroutine that’s running on the main thread is perfectly fine.
r
the effect i see as a user is that when we go to load the next page of results by sending a
Load
intent with a new page, the main thread hangs as the user scrolls and delays the scrolling animation until the work is done
@Zach Klippenstein (he/him) [MOD] for sure, i guess what i mean is that it feels like the main thread is being blocked
z
Hm, nothing in this code looks like it actually blocks. You’re sure
getPromotedProducts
isn’t the culprit?
Have you attached a debugger or added any trace statements to see where the block is occurring?
r
Copy code
fun getPromotedProducts(
        categorySlug: String,
        sortBy: String?,
        sortOrder: String?,
        offset: Int,
        limit: Int
    ): Flow<Either<List<BrandProduct>>> {
        // TODO need to solve for retry/error handling for Retrofit
        return flow {
            val userLocation = locationRepository.getLocation().receive().getOrNull()

            try {
                val result = userLocation?.run {
                    api.getPromotedProducts(
                            categorySlug = categorySlug,
                            latlng = "${this.latitude},${this.longitude}",
                            sortBy = sortBy,
                            sortOrder = sortOrder,
                            offset = offset,
                            limit = limit
                    ).run { success(this.data?.brandProducts!!) }
                } ?: Either.error<List<BrandProduct>>(ServerError)

                emit(result)
            } catch (e: Exception) {
                when (e) {
                    is HttpException -> {
                        Timber.d("HttpError ${e.response()?.errorBody()?.string()}")
                    }
                }

                emit(Either.error(ServerError))
            }
        }.flowOn(dispatcher)
    }
z
As an aside,
consumeAsFlow().collect
is unnecessary, you can just do
consumeEach
.
👍 1
r
the dispatcher used here is
<http://Dispatchers.IO|Dispatchers.IO>
@Zach Klippenstein (he/him) [MOD] i did a little debugging, and it looks like we are waiting for the getPromotedProducts call to finish. thing is that it doesn't seem to be doing anything blocking
z
dispatcher
here is
<http://Dispatchers.IO|Dispatchers.IO>
? (in the future, big code posts like this are generally easier to read when posted as snippets instead of inline)
r
sorry about that, yes it’s a
<http://Dispatchers.IO|Dispatchers.IO>
z
So
getPromotedProducts
isn’t even returning immediately?
r
it is returning immediately, but something around the collection of the flow causes this main thread hiccup
z
Another aside, this also looks like it could just be a simple suspend function, since it only emits one thing.
💯 1
r
yeah, this is more experimentation around making our repository layers return flows to make combining values and applying operators really seamless
z
So if you change the state to
Loaded
while you’re scrolling, and
Loaded
doesn’t include any items, does your RecyclerView keep its current list until it gets a new one from a
Loaded
state?
r
no, it currently throws everything out regardless of the number of items
z
So when you start loading you clear your RecyclerView?
r
no only once Loaded state is read from the Activity
are you thinking it’s more of an issue with my Activity/View logic? i was leaning that way because the coroutine stuff all seems to check out
z
I’m not sure, but I don’t see anything in this code that looks like it would affect your scrolling like you’ve described.
Just to verify, if you remove the
_state.value = Loading(page == 0)
line, does that change anything?
r
it doesn’t, just doesn’t show my progress bar. that was one of the first things i tried haha
z
Can you debug more and try to find out exactly what line is blocking? Also maybe try putting a
withContext(<http://Dispatchers.IO|Dispatchers.IO>) { }
around your entire
getBrandProducts
method body?
r
yeah for sure, i’ll be able to mess more with it tomorrow. thanks for the ideas, and walking through it with me. much appreciated! i’ll post back tomorrow when i’ve got an update
z
You could also use the Android Studio profiler to see which thread is being blocked and what the stack traces are during that blockage
Good luck!
r
Good idea, thank you!
@Matt Rea 👋
@Zach Klippenstein (he/him) [MOD] hey Zach, so i finally got some time to look at this problem more, and now I'm pretty darn sure that it has nothing to do with coroutines, and everything to do with Android itself i've include some videos below. one video has had an added
delay(1000)
to the network request
Flow
we make to fetch items, and the other video has no
delay
the video with no
delay
has the very obvious frame drops, and the one with a
delay(1000)
doesn't
my intuition points to a potential issue in where we are scrolling and loading content into the recycler adapter so fast, that we drop frames because the system can't handle that load within the 16ms window we have to draw everything
investigating more, but thought i owed you an update