https://kotlinlang.org logo
Title
r

Ryan Simon

06/11/2020, 12:51 AM
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
// 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) }
    }
😒uspend: 1
☘️ 1
z

Zach Klippenstein (he/him) [MOD]

06/11/2020, 12:53 AM
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

Ryan Simon

06/11/2020, 12:53 AM
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

Zach Klippenstein (he/him) [MOD]

06/11/2020, 12:56 AM
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

Ryan Simon

06/11/2020, 12:57 AM
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

Zach Klippenstein (he/him) [MOD]

06/11/2020, 12:57 AM
As an aside,
consumeAsFlow().collect
is unnecessary, you can just do
consumeEach
.
👍 1
r

Ryan Simon

06/11/2020, 12:57 AM
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

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:04 AM
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

Ryan Simon

06/11/2020, 1:04 AM
sorry about that, yes it’s a
<http://Dispatchers.IO|Dispatchers.IO>
z

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:05 AM
So
getPromotedProducts
isn’t even returning immediately?
r

Ryan Simon

06/11/2020, 1:06 AM
it is returning immediately, but something around the collection of the flow causes this main thread hiccup
z

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:07 AM
Another aside, this also looks like it could just be a simple suspend function, since it only emits one thing.
💯 1
r

Ryan Simon

06/11/2020, 1:09 AM
yeah, this is more experimentation around making our repository layers return flows to make combining values and applying operators really seamless
z

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:10 AM
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

Ryan Simon

06/11/2020, 1:12 AM
no, it currently throws everything out regardless of the number of items
z

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:13 AM
So when you start loading you clear your RecyclerView?
r

Ryan Simon

06/11/2020, 1:14 AM
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

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:16 AM
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

Ryan Simon

06/11/2020, 1:17 AM
it doesn’t, just doesn’t show my progress bar. that was one of the first things i tried haha
z

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:18 AM
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

Ryan Simon

06/11/2020, 1:19 AM
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

Zach Klippenstein (he/him) [MOD]

06/11/2020, 1:27 AM
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

Ryan Simon

06/11/2020, 1:29 AM
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