Assigned
Status Update
Comments
al...@google.com <al...@google.com> #2
Also doesn't work in AS 2024.3.2 Canary 4 and Compose 1.8.0-beta01
al...@google.com <al...@google.com> #3
Andrei: Is this the same as
za...@squareup.com <za...@squareup.com> #4
Triage notes: Needs investigation. Assigning for later.
za...@gmail.com <za...@gmail.com> #5
Could you share the AppScaffoldTopBar
function that fails here? This seems different from
Description
Compose needs an API that make producing a consistent asynchronous result simple.
Problem:
If an application is making asynchronous calls based on application state it is difficult to produce a result consistent to the application state at the time the function completes.
For example, consider the following code,
This example will show a "Loading" state until the user data can be retrieved. This is so common in Compose that it has its own helper function,
produceState
,This works well if
userId
is not itself state. If reading theuserId
is a state read, as, for example, ifuserId
is a state backed object, such as,In this case you may be tempted to simply write,
but this is subtly wrong; the lambda is not re-executed when
userId
changes so changes touserId
inscreenModel
will not update the result ofScreen
. You may be tempted to fix this by rewriting the function to,This has a number of issues. First, the data displayed for new values of
userId
is off by at least a frame as the change value foruserData
is not reset tonull
when the lambda restarts and even doing this in the lambda itself, such as,is too late. Second, the coroutine for
produceState
is not executed until the composition is applied with the result of using the old value ofuserData
. Once the coroutine executes, composition has already completed and will not restart for another frame. This guarantees that the data will be at least one frame out of date.To avoid this, you may be tempted to introduce a flow into
produceState
such as,Seems to solve the issue. You can also use the
null
trick above to ensure the loading state reappears iffetchUserData(userId)
has not returned by the start of the next frame. However,snapshotFlow()
observers a sequence of changes touserId
, not just the last one. This means that ifuserId
changes faster thanfetchUserData(userId)
can fetch the user data, the flow will be updating thevalue
to old values ofuserId
until it can catch up, which, in the best case, just flickers the UI. What is required is a flow that discards the result if it is not current and only have a value when it is the value for the current version ofuserId
. This can be done with some clever use flow but it has now become quite complicated and error prone.What is needed is an effect that allows
value
to benull
or the user data foruserId
but nothing else. In other words, there needs to be an primitive that is the asynchronous version ofderivedStateOf()
that is always consistent with the state the lambda passed to it reads so that the snapshot will only see either the sentinel value or a result consistent with the state of the snapshot.Potential Solution:
One potential solution would be a
TaskEffect
. In aTaskEffect
the lambda restarted whenever any values read in the lambda are modified. If it is already running it is cancelled and started over. The original screen can then be written,The complexity of ensuring the snapshot created for the
TaskEffect
is only applied when theuserId
is the same value as was passed intofetchUserData
is handled by theTaskEffect
. TheTaskEffect
above will only ever benull
or the data returned byfetchUserData
for the value ofuserId
currently inscreenModel
. IfuserId
(or any other state value) read by theTaskEffect
lambda changes, the coroutine is cancelled (if it hasn't already completed) and is restarted. The coroutine is also cancelled and restarted outside of composition which means, if it can complete before the next frame starts, the composition will never have to show the "Loading..." message.The
TaskEffect
can also have a wrapper similer toproduceState()
that removes some of the boilerplate of the above,Task effect will also handle the subtle issue that occurs when the instance of
screenModel
changes. As Compose will only produce a new instance of a lambda when the values captured by the lambda change, when the lambda passed toproduceTaskState
changes will also cause the task results to be discarded and the lambda to be restarted.