One Off the Slack

These articles summarize the discussion on a particular thread in the #compose channel of JetBrains’ Slack workspace for Kotlin developers. If you would like to read the original thread, you will need access to that workspace — sign up at https://slack.kotlinlang.org!

How Do I Write End-to-End Tests?

The indefatigable Colton Idle asked:

I finished my first compose app and now I want to write a few end-to-end tests to test critical paths. App launch. App sign up. App log in. View all movies. Click and view a single movie. That sort of thing.

How do I do that in a pure-compose app? Do I still just use espresso to string together clicks? The compose testing docs seem to be more about testing specific composables.

Google’s Zach Klippenstein replied:

Espresso is gonna be very painful to use for this directly, but you can use it together with the compose testing library (just use createAndroidComposeRule instead of createComposeTestRule)

Google’s Alex Vanyo went in greater depth:

If you’re in a completely pure-Compose app, you will probably find you don’t need Espresso at all. You can interop Espresso with the compose testing library as Zach mentioned, but if you only have Compose elements I don’t think Espresso will be super useful.

Testing specific composables ends up being the easiest and simplest to do (because you can isolate the components and state under test), but you can absolutely use the same set of APIs for larger end-to-end tests. If you put your root composable under test, you can make assertions, perform actions, etc.

JetNews has some very basic end-to-end tests that trigger navigation and check for content on different screens here: https://github.com/android/compose-samples/blob/main/JetNews/app/src/sharedTest/java/com/example/jetnews/JetnewsTests.kt

Colton eventually got that working (🎉), albeit with a few hacks. Suffice it to say that it is likely that we will need more to go on for complex tests, such as end-to-end tests.

Posted 2021-11-28, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1637612960322800


Why Does My List Not Recompose?

Colton Idle has this code:

class ScreenAState {
    val people = mutableStateListOf<PersonUIWrapper>()
}

data class PersonUIWrapper(var selected: Boolean, val person: Person)

…as well as:

vm.state.people.forEach {
    TextButton(onClick = {
    it.selected = true
}) {
  if (it.selected) {
      Icon(Icons.Default.Check, null)
  }

  Text(text = it.person.name)
}

Colton’s problem was:

but my code doesn’t recompose, and therefore doesn’t show a checkmark. Is my PersonUIWrapper supposed to use snapshot state internally for it’s two fields?

In general, an ordinary var is unlikely to do what you want, particularly with Compose. In this case, Colton is modifying selected, and nothing in the system really cares. It is just a var, and a var is not special. Nothing is paying attention to that var to do anything when it is changed.

Or, as Joseph-Hawkes-Cates put it:

the mutableStateList will recompose on changes to the list itself rather than changes to the internal state of the values in the list

Slack user nitrog42 pointed to this similar Stack Overflow question.

Google’s Alex Vanyo offered:

To partially answer your original question, if PersonUIWrapper was backed by state that was observable to Compose (like mutableStateOf), then this would work like you’d expect.

And that can be preferable as an alternative to the .copy solution with a data class.

Colton adapted his wrapper to be:

class PersonUIWrapper(val selected: MutableState<Boolean> = mutableStateOf(true), val person: Person)

…but he was not completely happy:

It kind of sucks that I can’t use by in this case, but using .value isn’t the worst thing!

Google’s Adam Powell then offered this approach:

class PersonUIWrapper(
  selected: Boolean,
  val person: Person
) {
  var selected by mutableStateOf(selected)
}

Here, selected is both a constructor parameter and a var property backed by the mutableStateOf() property delegate. The constructor parameter is used for the initial value of the property. Now, you can refer to selected like an ordinary var, while it still is a MutableState and participates in the recomposition system.

Posted 2021-11-20, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1637187114148800


When Do Orientation Changes Take Effect?

Rick Regan asked:

I’m tracing through composition during device rotation and I see behavior I wasn’t expecting. I ran this composable in a Pixel 4A on the emulator:

@Composable
fun Dimensions() {
  BoxWithConstraints {
    val orientation = if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) "P" else "L"
    println("$orientation: w = $maxWidth, h = $maxHeight")
  }
}

I started in portrait orientation and then rotated the device left 8 times (with android:screenOrientation="fullUser" to allow it to render upside down). There were three unexpected things about the output:

(1) Duplicate messages for a given rotation

(2) Change in orientation is not in sync with the change in dimensions

(3) Incorrect intermediate dimensions

(1) I suppose is just Compose working as advertised (duplicate recompositions), but (2) and (3) I don’t understand.

Google’s Adam Powell explained:

Composition happens before layout does, and layout is when constraints are determined. The system doesn’t know at composition time that the layout constraints are going to change, (the composition you’re about to perform can determine that,) and the BoxWithConstraints subcomposition recomposes in the normal recompose step as a result of LocalConfiguration changing. Then layout happens and the contents are measured with the new constraints, so BoxWithConstraints recomposes again, now with the new constraints.

Rick was seeing rotation artifacts:

When I hit a breakpoint in my app during rotation to landscape I can see the top part of my portrait layout in the left half of the screen (the right half is black). At full-speed on a device I can’t see that, though now I wonder if this is why rotation in my app never looked as smooth as rotation in the Gmail or Drive apps, for example.

Adam clarified:

the additional recomposition I described happens in the same frame before drawing

Rick still was concerned:

Ok thanks. Not knowing the internals too well I didn’t know you could have multiple compositions per frame. But this is consistent with the testing I was just doing – trying to capture the rotation on a device by recording it at 60fps on another. I never did see that “half frame” that I see on the emulator. I guess my app’s lack of smoothness is just in my head 🙂

Adam pointed out a possible reason for the behavior:

well, it could also be that the recompositions involved are just running a lot of code to perform that recomposition. 🙂 https://ui.perfetto.dev and placing your own trace points can be a good way to see where some of the time is going in a release build

Posted 2021-11-12, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1636385075109100


More!

Older “One Off the Slack” articles can be found in the archives.