https://kotlinlang.org logo
#compose
Title
# compose
z

zak.taccardi

09/01/2020, 5:26 PM
flow.collectAsState("initialState")
requires an initial state (when not using a
StateFlow<T>
. How can we avoid having this initial state? For example, I would like to have a composable not be considered “ready” until the
Flow<T>
emits. By ready, I mean shown to the user. Is something like this possible? The idea would be something like
fragment.postponeEnterTransition(..)
(link) which allows the
Fragment
to asynchronously load its data before appearing to the user. This way an initial state would not need to be provided and only fully loaded data need be displayed
👍 4
this would allow Compose to avoid displaying a “bad loading state” where an old screen continues to be shown until the new screen is ready and loaded https://reactjs.org/docs/concurrent-mode-intro.html#intentional-loading-sequences
d

dimsuz

09/01/2020, 5:43 PM
can't you make a composable which renders your "no state" state? Like an empty
Surface
or something like this.
z

zak.taccardi

09/01/2020, 5:43 PM
the idea is that I would actually want to avoid even showing the “no state” state at all
d

dimsuz

09/01/2020, 5:44 PM
but what would you show? I mean that screen is always in some state. so "no state" is a state. Or perhaps I misunderstood your case.
z

zak.taccardi

09/01/2020, 5:44 PM
like I would want to show
ComposableA
while
ComposableB
is still in its “no state” loading state
d

dimsuz

09/01/2020, 5:46 PM
Ah. I see. Maybe something like
Copy code
flow.collectAsState(Optional.None).filter { it !is None }
but this can probably be considered a work around 🙂
z

zak.taccardi

09/01/2020, 5:46 PM
well it’s Kotlin so we can use nulls
👌 1
but I don’t want to deal with
State<T?>
at all, just
State<T>
d

dimsuz

09/01/2020, 5:51 PM
in Rx we have
.filterSome
which works like this
Copy code
myStream // Observable<Optional<T>>
  .filterSome() // Observable<T>
  .map { value: T -> /* do  something */ }
So
filterSome
does the necessary casting inside. Perhaps something like this can be done here.
a

Adam Powell

09/01/2020, 5:52 PM
val thing = flow.observeAsState(null).value ?: return
?
👍 2
we've had a bunch of discussions around what it might mean to allow a
@Composable suspend fun
and what kinds of fallback display constructs that might entail if it suspends rather than completes, but any of that is going to be post-1.0 stuff
👍 3
you might be able to have some fun with a
Copy code
flow<@Composable () -> Unit> { ... }
and
Copy code
flow.observeAsState({ Fallback() }).value()
h

Halil Ozercan

09/01/2020, 6:02 PM
I kinda understand the point of always requiring an initial state but what annoyed me was mapping a
StateFlow
to another
Flow
. Since it becomes a regular
Flow
,
collectAsState
expects an initial state but in reality it already has an initial state.
h

Halil Ozercan

09/01/2020, 6:04 PM
Copy code
@Composable
inline fun <T, R> StateFlow<T>.collectMap(
    context: CoroutineContext = Dispatchers.Main,
    crossinline block: T.() -> R
): State<R> = map { it.block() }.collectAsState(value.block(), context)
This was what I had come up with 😄, looks similar
z

zak.taccardi

09/01/2020, 6:07 PM
@Adam Powell what happens if you
return
out of a composeable before rendering anything?
a

Adam Powell

09/01/2020, 6:08 PM
nothing special, it simply doesn't emit any UI
👍 1
@Halil Ozercan be careful with that, it's missing a
remember
🙂
assembling flow (or rx) operator chains in a composable function gets a little bit fun, doing it correctly for the above looks like:
Copy code
remember(this) { map { it.block() } }.collectAsState(...)
otherwise you get a new instance of the operator chain each time you recompose, which means
.collectAsState
will cancel the old collection and launch a new one
r

Ricardo C.

09/01/2020, 7:30 PM
There's a StateFlow implementation that omits that initial parameter already:
we need this on Flow though. It's very easy to land on Flow after applying some operators to StateFlow. And I'm also not sure if we should be exposing StateFlow instead of Flow as it kinda seems we're leaking implementation details
a

Adam Powell

09/01/2020, 8:32 PM
There is no initial value to use on
Flow
🙂 we can't know it will emit without suspending until we try it, and beginning collection is a side effect that happens deferred once a composition lands, not synchronously.
g

gildor

09/01/2020, 11:44 PM
It's fine to expose StateFlow, because it has different contract comparing to Flow (it provides value), also it read only. So works perfectly for cases "flow with default value", such as this one
h

Halil Ozercan

09/03/2020, 8:35 PM
Sorry if this is poking an already closed thread but it's still hard for me to getting used to this approach
Copy code
remember { commentsViewModel.isCommentHidden(comment.id) }.collectAsState(initial = false)
Although I understand how remember works and its general functionality in composables, I feel like subscribing to a flow shouldn't require me to be careful about that this call(
.collect
) will be re-executed in case of recomposition. Is there any plan in the future like introducing a new API for flow subscriptions without a need for
remember
?
a

Adam Powell

09/03/2020, 11:10 PM
the subscriptions don't need
remember
, the assembly does
and perhaps making things worse for your example, the code there likely isn't correct, you need a key to tell remember when to recalculate
it's likely to be something more like
Copy code
remember(commentsViewModel, comment.id) { commentsViewModel.isCommentHidden(comment.id) }.collectAsState(initial = false)
to your point though, yes I've been playing with some related API shapes that would make assembly+collection at the same call site a bit less unsightly
it would look more like
Copy code
val current by produceState(initial = false, commentsViewModel, comment.id) {
    commentsViewModel.isCommentHidden(comment.id)
        .collect { setValue(it) }
}
more generalized, assemble the flow outside of composition in a suspending block where you also collect it instead and as a result you're on the other side of what compose
but you lose the snazzy one-liner, and it still doesn't solve the case where someone calls
Copy code
MyComposable(flow.map { thing(it) })
instead of
Copy code
MyComposable(remember(flow) { flow.map { thing(it) } })
some other things can help out a bit in that area as well though, for example something like
Copy code
flow.rememberLet { it.map { thing(it) } }.collectAsState(initial)
👍 1
which helps in not breaking the fluent chain
but since your viewmodel example calls a function that returns a flow, presumably a new instance each time, both the viewmodel and the comment id are probably required as remember keys
h

Halil Ozercan

09/04/2020, 9:54 AM
Copy code
flow.rememberLet { it.map { thing(it) } }.collectAsState(initial)
This looks way more fluent and recognizable. My example resides in an item view(keyed) of a list, so I didn't think I needed to use viewModel instance and id as arguments to remember. I'm not sure how
LazyFor
operates but if my first item gets deleted, would the first composable gets recomposed with second item's properties or does it just leave the UI? If former is the case, remember should definitely receive those arguments.
a

Adam Powell

09/04/2020, 1:48 PM
The lazy UI composables all internally use a
key
so individual items won't get recomposed for content with a different identity. Sometimes you can situationally omit these things when using remember if you know exactly how they'll be used, but it's a potential surprise for later if you use them elsewhere and those assumptions no longer hold.
270 Views