https://kotlinlang.org logo
l

Lilly

06/12/2020, 10:18 PM
On Start
ScannerScreenContent
ist called twice and everytime
val state by viewModel.device.observeAsState()
is triggered,
ScannerScreenContent
is called again which leads to a list with always 1 item. Does someone have an idea?
z

Zach Klippenstein (he/him) [MOD]

06/12/2020, 10:28 PM
How does
viewModel.device
work? Does it just emit every time there’s a new device?
l

Lilly

06/12/2020, 10:34 PM
Yes exactly.
Copy code
val device: LiveData<BluetoothDeviceWrapper>
    get() = bluetoothManager.device
z

Zach Klippenstein (he/him) [MOD]

06/12/2020, 10:45 PM
This isn’t really state then, so using
observeAsState()
isn’t really ideal. This would be a great use case for
launchInComposition
though. Basically you could do something like:
Copy code
// Key the coroutine on the device Observable, so if the observable changes for some reason you'll resubscribe.
launchInComposition(viewModel.device) {
  // Now you're in a coroutine that will run
  // continuously across recompositions.
  viewModel.device.asFlow().collect { device ->
    modelList.add(device)
  }
}
This would replace lines 22-28 of your snippet. https://developer.android.com/reference/kotlin/androidx/compose/package-summary#launchincomposition_1
l

Lilly

06/12/2020, 10:49 PM
ok thanks, I will try it. And what do you think about the fact that Scaffold bodyContent is calling twice? I'm on dev13 but dev12 same problem, is this a bug?
z

Zach Klippenstein (he/him) [MOD]

06/12/2020, 10:50 PM
When you say it’s being called twice, do you mean the lambda it returns is invoked twice? Like for example, line 23 is executed twice in a row?
l

Lilly

06/12/2020, 10:53 PM
Yes the lambda is called twice:
Copy code
bodyContent = {
  Log.d("Debug", "This is called twice!")
}
Maybe its in your project too and you didnt notice
z

Zach Klippenstein (he/him) [MOD]

06/12/2020, 11:14 PM
That doesn’t seem surprising. It just means that somehow compose is deciding to do two composition passes of your function. Not sure why, but it shouldn’t really matter. As far as i can see the only reason why it’s an issue for your code is that you’re doing side effects directly in your function, which would be fixed by that
launchInComposition
snippet.
l

Lilly

06/13/2020, 12:01 AM
I dont know how to use channels, so I tried this:
Copy code
@Composable
fun ScannerScreenContent(
    onItemAction: () -> Unit
): @Composable() (Modifier) -> Unit = { modifier ->
    val viewModel = ScannerViewModelAmbient.current

    val context = ContextAmbient.current as MainActivity

    val modelList = modelListOf<BluetoothDeviceWrapper>()

    launchInComposition(viewModel.device) {
        viewModel.device.observe(context, Observer {
            Log.d("Found", "here")
            modelList.add(it)
        })
    }

    AdapterModelListComponent(modelList, modifier, onItemAction)
}
but the AdapterList is not updating on new devices. modelList is not empty.
I modified my code:
Copy code
val modelList = modelListOf<BluetoothDeviceWrapper>()

launchInComposition(modelList) {
    viewModel.device.asFlow().collect { device ->
        modelList.add(device)
        Log.d("Found", "size: ${modelList.size}")
    }
}
Now it works. I had to change
launchInComposition(viewModel.device)
to
launchInComposition(modelList)
. To avoid duplicates I just write
Copy code
if (modelList.contains(device)) return@collect
right?
z

Zach Klippenstein (he/him) [MOD]

06/13/2020, 2:05 AM
For duplicates, that works, although it would be more idiomatic kotlin to do
if (device in modelList)
, but that’s really just a style matter.
One other thing, I thing you need to wrap
modelListOf()
in a
remember
, so
Copy code
val modelList = remember { mutableListOf<BluetoothDeviceWrapper>() }
Otherwise you’re creating a new
ModelList
on every compose pass, so I’m not sure why that would work at all
You also shouldn’t need to pass the list to
launchInComposition
, since that would restart the coroutine every time the list instance changes
l

Lilly

06/13/2020, 3:18 PM
Hmm when I do this
Copy code
@Composable
fun ScannerScreenContent(
    onItemAction: () -> Unit
): @Composable() (Modifier) -> Unit = { modifier ->
    val viewModel = ScannerViewModelAmbient.current

    val modelList = remember { mutableListOf<BluetoothDeviceWrapper>() }

    launchInComposition(viewModel.device) {
        viewModel.device.asFlow().collect { device ->
            if (device in modelList) return@collect
            modelList.add(device)
            Log.d("Found", "size: ${modelList.size}")
        }
    }

    AdapterModelListComponent(modelList, modifier, onItemAction)
}
The AdapterList is not updating, that says, no items are listed. Any ideas? 😕
z

Zach Klippenstein (he/him) [MOD]

06/13/2020, 3:27 PM
Sorry, that should be
modelListOf
, not
mutableListOf
l

Lilly

06/13/2020, 3:32 PM
Haha wow my bad 😄 I didn't realize that, now it works!
Can you explain me please, when I have to use
remember
, I mean when I don't make something stupid the function
ScannerScreenContent
wouldn't be called again, so modelList would always remain the same.
z

Zach Klippenstein (he/him) [MOD]

06/13/2020, 4:06 PM
You should always use one of the state memoization functions for initializing state because not doing so is brittle. It's brittle because it's really not under the control of the function how many times it's called. If any code calling the function decides to pass a different value for any of your parameters, it will be re-invoked. Even if your function is private and you have full control of all the callsites, this would be hard to maintain since it's unidiomatic and would need to be called out explicitly in the documentation, and easy to miss for new developers or remember for yourself when you come back to this code later. Basically, it would behave in a way that would be surprising compared to other Compose code, and it could break as the result of all kinds of seemingly unrelated changes over time. It's also easy to break by changing the code within the function to something that triggers recomposition of the function itself. Developers shouldn't have to think about the specifics of how often a composable function is invoked, and making the correct behavior of your code dependent on that is going to be hard to debug later. Also, the upper bound of the number of times a composable function is called is effectively an implementation detail of the compose runtime. The runtime guarantees, as a lower bound, that you will eventually be recomposed if some state you depend on changes, but afaik doesn't make guarantees about the upper bound of how often it will be called. The fact that it's only called when something changes is really an optimization, in my mind at least.
l

Lilly

06/13/2020, 4:48 PM
Nice explanation, thank you very much, I guess I got it now. And what about
launchInComposition
. Is it used to call blocking code + a concise way of doing something in a coroutine within a composable? I mean when I place
Copy code
viewModel.device.asFlow().collect { device ->
            if (device in modelList) return@collect
            modelList.add(device)
            Log.d("Found", "size: ${modelList.size}")
        }
directly in the composable function it would block the code. What did you mean with side effect...I found these words in the docs too.
I'm also wondering why my old code triggered a recompose of the
ScannerScreenContent
function:
Copy code
val state by viewModel.device.observeAsState()
    state?.let { device ->
        modelList.add(device)
        modelList.forEach {
            Log.d("Items", it.bluetoothDevice.name)
        }
    }
Sorry for the annoying questions
z

Zach Klippenstein (he/him) [MOD]

06/13/2020, 6:28 PM
what about 
launchInComposition
. Is it used to call blocking code + a concise way of doing something in a coroutine within a composable?
It launches a new coroutine the first time it is included in the composition at a given position. That coroutine will remain running until the
launchInComposition
call is removed from the composition (i.e. if you surround the call in an
if
and eventually stop calling it). It is very useful for performing long-running asynchronous actions (such as collecting a flow/subscribing to an Observable), or for performing long-running side effects in the background.
I’m also wondering why my old code triggered a recompose of the 
ScannerScreenContent
 function
I believe it’s because
ModelList
is a type that is tracked by the compose runtime, so when you read from it directly in a Composable function body, the runtime knows to re-invoke your function when the list changes. Your code was reading from the list (via
forEach
) immediately after creating the list, which I think is what triggered the second composition. Moving the read into the
launchInComposition
fixed the issue because the lambda passed to
launchInComposition
isn’t a Composable function, so reading from it in that context doesn’t trigger a recomposition.
👍 1
Sorry for the annoying questions
Not annoying! 🙂 These are subtle behaviors, and the
launchInComposition
documentation could probably some more detail about how/why/when to use it. This is probably also helpful for the team to figure out what kind of stuff it is important to cover in the guides as they flesh out the documentation.
l

Lilly

06/13/2020, 7:00 PM
Really nice explained, I got it. Thank you so much! 🙂 Where do you get those "best practice" informations?
z

Zach Klippenstein (he/him) [MOD]

06/13/2020, 7:07 PM
Mostly just from following this channel and reading what the compose team members have to say about this stuff. There are also a few talks and podcasts with them that explain some of this stuff as well. One good thread about the dangers of performing side effects directly in Composables was this: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1591659323449600?thread_ts=1591653158.447300&amp;cid=CJLTWPH7S
l

Lilly

06/15/2020, 10:49 AM
Makes sense. I guess the members with a [G] in the name are compose team members or at least members of google right?
Indeed one of them answered me, he also explained that "its almost impossible to know how many times any particular components in the code will be recomposed." Sweet!