Steffen Funke
10/25/2022, 5:40 PMunstable
/ not skippable
item views in LazyList
=> đź§µSteffen Funke
10/25/2022, 5:46 PMLazyList
with some Item Views (ProductListItemView
), which should load their data as they scroll into view. For that, a suspending lambda getProduct
is passed in (e.g. from outer ViewModel).
@Immutable
data class Product(
val id: String,
val name: String
)
@Composable
fun ProductListItemView(
productId: String,
modifier: Modifier = Modifier,
// unstable bc of suspend function
getProduct: suspend (productId: String) -> Result<Product>
) {
val currentGetProduct by rememberUpdatedState(newValue = getProduct)
var result by remember { mutableStateOf<Result<Product>>(Result.Loading) }
LaunchedEffect(key1 = productId) {
result = currentGetProduct(productId)
}
when(result) {
is Loading -> {
// Show Loading UI
}
Result.Success -> {
// show success UI
}
}
}
I assumed this approach would be nice in terms of encapsulation and testing, but seemingly breaks the “skippability” of the composable (as seen in compose compiler report and metrics):
restartable scheme("[androidx.compose.ui.UiComposable]") fun ProductListItemView(
stable productId: String
unstable getProduct: SuspendFunction1<@[ParameterName(name = 'productId')] String, Result<Product>>?
...
)
As far as I understand, this leads to more recompositions than I actually (wrongfully) assumed.
I am wondering myself wether I am on the wrong foot with that approach (passing in a suspending function, and calling it in the LaunchedEffect)?
Anyone knowing a better way to trigger a delayed external data load, e.g. when the ListItem comes into view?Steffen Funke
10/25/2022, 5:49 PMChris Johnson
10/25/2022, 6:16 PMsuspend
qualifier for your function in that param. All your composable needs to know is that it is a lambda.
The actual declaration/implementation site of the function should be suspend so that inside your composable it does need to be in a coroutine scope.
So if you remove the suspend keyword it should be what you're expectingSteffen Funke
10/25/2022, 6:22 PMsuspend
keyword seems to be part of the method signature. If I remove it, at some point up the caller chain I hit the wall:Steffen Funke
10/25/2022, 6:27 PMgetProduct
Method (which gets passed down to the composable) I can not remove the suspend
that easily.
suspend fun getProduct(productId: String): Result<Product> {
// do actual stuff
}
Chris Johnson
10/25/2022, 6:30 PM@Immutable
interface UiActions {
suspend fun getProduct()
}
@Composable
fun Main() {
content(viewModel)
}
@Composable
fun content(actions: UiActions) {
LaunchedEffect(actions) {
actions.getProduct()
}
}
Steffen Funke
10/25/2022, 6:37 PMfun bla()
and suspend fun bla()
do not seem to go along.
It is either both, interface and implementation to be suspend
, or none of them.
Or am I misunderstanding your suggestion? 🤔Chris Johnson
10/25/2022, 6:39 PMsuspend
to your getProduct definition in your interface.
suspend fun getProduct(productId: String): Product
Steffen Funke
10/25/2022, 6:40 PMUiActions
into the composable. That might work.Steffen Funke
10/25/2022, 6:41 PMChris Johnson
10/25/2022, 6:41 PM@Immutable
annotation because interfaces aren't stable by default. Meaning it would make it not skippableshikasd
10/26/2022, 12:16 AMsuspend fun
lambdas should also be marked as stable here 🙂Leland Richardson [G]
10/26/2022, 4:51 PMLeland Richardson [G]
10/26/2022, 4:53 PMLeland Richardson [G]
10/26/2022, 4:54 PMProductListItemView
looks like?shikasd
10/26/2022, 5:07 PMSteffen Funke
03/14/2023, 9:37 AMMainViewModel
with a normal and a suspending function
• Greeting
Composable function that references those two as lambdas and uses them
• ⇒ normal lambda is marked stable and skippable ✅
• ⇒ suspending lambda is marked unstable {thus not skippable?) ⚠️
class MainViewModel : ViewModel() {
fun getData(name: String): String {
return "result: $name"
}
suspend fun getDataSuspending(name: String): String {
delay(1000)
return "result: $name"
}
}
class MainActivity : ComponentActivity() {
private val viewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting(
name = "Android",
getDataSuspending = viewModel::getDataSuspending,
getData = viewModel::getData,
modifier = Modifier.fillMaxSize()
)
}
}
}
@Composable
fun Greeting(
name: String,
modifier: Modifier = Modifier,
getDataSuspending: suspend (String) -> String,
getData: (String) -> String
) {
val scope = rememberCoroutineScope()
Column(modifier = modifier) {
Button(onClick = { scope.launch { getDataSuspending(name) } }) {
Text("Launch suspend function")
}
Button(onClick = { getData(name) }) {
Text("Launch regular lambda")
}
}
}
Steffen Funke
03/14/2023, 9:38 AMrestartable scheme("[androidx.compose.ui.UiComposable]") fun Greeting(
stable name: String
stable modifier: Modifier? = @static Companion
unstable getDataSuspending: SuspendFunction1<String, String>
stable getData: Function1<String, String>
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun GreetingPreview()
Steffen Funke
03/14/2023, 9:40 AMto be clear, a suspend lambda is stable. i think the problem here is that the lambda isn’t getting memoized at the call site, so it is a different instance, and thus not skippingAs far as I can see, this example shows that the (suspending) Lambda is the same instance (a viewmodel reference) and does not change, or am I mistaken in my assumptions?
Steffen Funke
03/14/2023, 9:47 AMshikasd
03/14/2023, 1:43 PMviewModel::someMethod
twice will yield the same instance, tbh
You can check with ===Steffen Funke
03/15/2023, 7:02 AM