© Instil Software 2020
Compose Part 2
(the practice)
Belfast Google Dev Group
September 2022
@kelvinharron
https://instil.co
– Short history tour of Android UI Development.
– Examples of using Jetpack Compose in across multiple
apps.
– Focus on Navigation, Accessibility & Testing.
– Sneak peak at an upcoming Instil app!
AGENDA
1
A history of
Android UI dev
The classic - findViewById XML layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
id used by Activity
The classic - findViewById Activity
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById<TextView?>(R.id.textView).apply {
text = "Hello GDE"
textSize = 42f
setTextColor(Color.CYAN)
}
}
Instil recommendation - Databinding
<layout ...>
<data>
<import type="android.view.View"/>
<variable
name="vm"
type="co.instil.databinding.DemoViewModel"/>
</data>
<TextView
...
android:text="@{vm.text}"/>
</LinearLayout>
</layout>
Databinding XML
layout as root
source of truth
binded field
class DemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityDemoBinding>
(this,R.layout.activity_demo)
binding.vm = DemoViewModel()
}
}
Databinding Activity
XML layout file
setting vm on XML
class DemoViewModel {
val text = ObservableField("Data binding works!")
}
ViewModel
The shiny new thing, Jetpack Compose!
UI representing State
… but starting from scratch?
2
Working with
Compose
WIP
Sharing Components
Previews that work!
@Composable
@Preview(uiMode = UI_MODE_NIGHT_YES)
fun CarouselScreenDarkPreview() {
EstiMateTheme {
CarouselScreen(
{},
{ CardSelection.Nuclear },
CardSelection.Nuclear
)
}
}
Card(
backgroundColor = instilGreen,
shape = RoundedCornerShape(size = 16.dp),
elevation = 8.dp,
modifier = modifier
.padding(8.dp)
.width(120.dp)
.height(150.dp)
.clickable { onSelected(selection) },
) {
CardContent(
selection = selection,
fontSize = fontSize
)
}
EstiMateCard
default modifier
material component
Box(contentAlignment = Alignment.Center) {
when (selection) {
is CoffeeBreak -> CoffeeImage()
is Nuclear -> NuclearImage()
else -> SelectionText(selection, fontSize)
}
}
CardContent
`Flow` of data
StateFlow<List<Player>>
ViewModel
Composable
Screen
Jetpack
DataStore
Firebase
Flow<List<Player>>
List<Player>
open fun getPlayers(teamName: String): Flow<List<Player>> = callbackFlow {
firebaseService.observePlayersIn(teamName) { players ->
trySend(players)
}
awaitClose { channel.close() }
}
PlayerService calling to Firebase
posting a fresh list of players
val players: StateFlow<List<Player>> = playerService.getPlayers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = emptyList()
)
ViewModel called to PlayerService
cancels the work once vm cleared
val votedPlayers: List<Player> by viewModel.players.collectAsState()
...
Composable calling to ViewModel
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(votedPlayers.size) { index ->
UserVoteItem(votedPlayers[index])
}
}
composable per list item
listOf(
Player(CardSelection.Five, "Kelvin"),
Player(CardSelection.Three, "Garth")
)
– Started in 2016.
– MVP pattern through use of interfaces.
– Jetpack Compose introduced as we took ownership of
development.
– Shipped 2 new UI driven features with Jetpack Compose.
Vypr
Android App
Bumping old dependencies
Using Jetpack Compose in XML Views
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
reference id
Using Jetpack Compose in XML Views
private var _binding: FragmentSteersBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSteersBinding.inflate(inflater, container, false)
val view = binding.root
binding.composeView.apply {
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
setContent {
VyprTheme {
SteersListView()
}
}
}
return view
}
compose_view xml element
our new composable
Using XML views in Jetpack Compose
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
AspectRatioImageView(context).apply {
load(steer.previewImageUrl)
setOnClickListener { onSteerClicked(steer) }
}
}
)
legacy view with lots of scary
– RecyclerView & adapter complexity removed.
– Jetpack Compose views driven by lifecycle aware ViewModel.
– More testable implementation.
– We fixed the bug!
Steers List Interop example
3
Navigation
Activity to Activity
Activities with Fragments
Single Activity Architecture
Jetpack Compose Navigation!
– NavController - central API for stateful navigation.
– NavHost - links NavController with a navigation graph.
– Each Composable screen is known as a route.
Jetpack Navigation
Now supporting Compose
Compose Navigation!
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
Navigating to another Route
navController.navigate("friendslist")
Compose Navigation
With Arguments
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType
})
) {...}
}
...
navController.navigate("profile/user1234")
compose-destinations
A KSP library that processes annotations and
generates code that uses Official Jetpack
Compose Navigation under the hood.
It hides the complex, non-type-safe and boilerplate
code you would have to write otherwise.
Rafael
Costa
github.com/raamcosta/compose-destinations
Adding a Destination
@Destination(start = true)
@Composable
fun LoginRoute(
destinationsNavigator: DestinationsNavigator
) {
LoginScreen(
...
)
}
tag composable for generation
provided for nav
Generated NavGraph
object NavGraphs {
val root = NavGraph(
route = "root",
startRoute = LoginRouteDestination,
destinations = listOf(
CarouselRouteDestination,
LoginRouteDestination,
ResultsRouteDestination,
SelectionRouteDestination
)
)
}
each annotated composable
labelled start
Type safe navigation
onLoginClicked = {
destinationsNavigator.navigate(SelectionRouteDestination)
},
Type safe navigation with args
onCardSelected = { cardSelection ->
destinationsNavigator.navigate(
CarouselRouteDestination(selection = cardSelection)
)
})
Custom Serializer
@NavTypeSerializer
class CardSelectionSerializer : DestinationsNavTypeSerializer<CardSelection> {
override fun fromRouteString(routeStr: String): CardSelection {
return CardSelection.fromString(routeStr)
}
override fun toRouteString(value: CardSelection): String {
return value.name
}
}
Under the hood
object LoginRouteDestination : DirectionDestination {
operator fun invoke() = this
@get:RestrictTo(RestrictTo.Scope.SUBCLASSES)
override val baseRoute = "login_route"
override val route = baseRoute
@Composable
override fun DestinationScope<Unit>.Content(
dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<Unit>.() -> Unit
) {
LoginRoute(
destinationsNavigator = destinationsNavigator
)
}
}
our composable
4
Accessibility
What is Accessibility?
“Mobile accessibility” refers to making websites
and applications more accessible to people with
disabilities when they are using mobile phones
and other devices.
Shawn Lawton
Henry
w3.org/WAI/standards-guidelines/mobile/
Android Accessibility
– Switch Access: interact with switches instead of the
touchscreen.
– BrailleBack: refreshable Braille display to an Android device
over Bluetooth.
– Voice Access: control an Android device with spoken
commands.
– TalkBack: spoken feedback for UI interactions.
What options are baked into the OS?
TalkBack
An essential tool for every Android team
Play Store Testing
Play Store Testing - Example Issue
Screen Reader issue confirmed
no context of dialog
Fixing the Dialog
val accountDeletionDialogAccessibilityLabel = stringResource(
id = R.string.accessibility_account_deletion_delete_dialog
)
AlertDialog(
...
modifier = Modifier.semantics(mergeDescendants = true) {
contentDescription = accountDeletionDialogAccessibilityLabel
}
)
Fixed!
Accessibility Starts with Design
Screen Sizes & Resolution
UI Mockups
– Discuss what the UI toolkit can do when size is
constrained. Compose is good at scaling text!
– Agree how to handle view scaling.
– Agree copy for accessibility labelling.
– Collaborate with designers & product owners.
5
Testing
Compose Semantics
Semantics, as the name implies, give meaning to a
piece of UI. In this context, a "piece of UI" (or element)
can mean anything from a single composable to a full
screen.
The semantics tree is generated alongside the UI
hierarchy, and describes it.
Example Button
Button(
modifier = Modifier.semantics {
contentDescription = "Add to favorites"
}
)
individual ui elements make up a button
easier to find
Test Setup
@get:Rule
val composeTestRule = createAndroidComposeRule<VyPopsActivity>()
@Before
fun beforeEachTest() {
composeTestRule.setContent {
VyprTheme {
VyPopsLandingScreen(EmptyDestinationsNavigator)
}
}
}
Finders
Select one or more elements (nodes) to assert or act on
composeTestRule
.onNodeWithContentDescription("Close Button")
composeTestRule
.onNodeWithText("What happens next")
Finders - Debug Logging
Node #1 at (l=0.0, t=54.0, r=720.0, b=1436.0)px
|-Node #2 at (l=70.0, t=54.0, r=650.0, b=1436.0)px
ContentDescription = '[VyPops Permissions Page]'
|-Node #3 at (l=70.0, t=75.0, r=112.0, b=117.0)px
| Role = 'Button'
| Focused = 'false'
| ContentDescription = '[Close Button]'
| Actions = [OnClick]
| MergeDescendants = 'true'
|-Node #6 at (l=229.0, t=194.0, r=492.0, b=303.0)px
| ContentDescription = '[Vypr Logo]'
| Role = 'Image'
|-Node #7 at (l=91.0, t=687.0, r=133.0, b=729.0)px
| ContentDescription = '[Record Audio Tick]'
| Role = 'Image'
|-Node #8 at (l=147.0, t=684.0, r=615.0, b=731.0)px
| Text = '[Microphone access granted]'
| Actions = [GetTextLayoutResult]
|-Node #9 at (l=125.0, t=762.0, r=167.0, b=804.0)px
| ContentDescription = '[Camera Tick]'
| Role = 'Image'
|-Node #10 at (l=181.0, t=759.0, r=582.0, b=806.0)px
| Text = '[Camera access granted]'
| Actions = [GetTextLayoutResult]
|-Node #11 at (l=84.0, t=1275.0, r=636.0, b=1366.0)px
Text = '[VyPops needs access to both your camera and microphone.]'
Actions = [GetTextLayoutResult]
Node #1 at (l=0.0, t=54.0, r=720.0, b=1436.0)px
|-Node #2 at (l=70.0, t=54.0, r=650.0, b=1436.0)px
ContentDescription = '[VyPops Permissions Page]'
|-Node #3 at (l=70.0, t=75.0, r=112.0, b=117.0)px
| Role = 'Button'
| Focused = 'false'
| Actions = [OnClick]
| MergeDescendants = 'true'
| |-Node #5 at (l=70.0, t=75.0, r=112.0, b=117.0)px
| ContentDescription = '[Close Button]'
| Role = 'Image'
|-Node #6 at (l=229.0, t=194.0, r=492.0, b=303.0)px
| ContentDescription = '[Vypr Logo]'
| Role = 'Image'
|-Node #7 at (l=91.0, t=687.0, r=133.0, b=729.0)px
| ContentDescription = '[Record Audio Tick]'
| Role = 'Image'
|-Node #8 at (l=147.0, t=684.0, r=615.0, b=731.0)px
| Text = '[Microphone access granted]'
| Actions = [GetTextLayoutResult]
|-Node #9 at (l=125.0, t=762.0, r=167.0, b=804.0)px
| ContentDescription = '[Camera Tick]'
| Role = 'Image'
|-Node #10 at (l=181.0, t=759.0, r=582.0, b=806.0)px
| Text = '[Camera access granted]'
| Actions = [GetTextLayoutResult]
|-Node #11 at (l=84.0, t=1275.0, r=636.0, b=1366.0)px
Text = '[VyPops needs access to both your camera and microphone.]'
Actions = [GetTextLayoutResult]
Assertions
Verify elements exist or have certain attributes
composeTestRule
.onNodeWithContentDescription("Login Button")
.assertIsEnabled()
composeTestRule
.onNodeWithText("What happens next")
.assertIsDisplayed()
Simulate user input or gestures
Actions
composeTestRule
.onNodeWithContentDescription("Close Button")
.performClick()
...
.performTouchInput {
swipeLeft()
}
developer.android.com/jetpack/compose/testing-cheatsheet
Shot is a Gradle plugin and a core android library
thought to run screenshot tests for Android.
Pedro
Gómez
github.com/pedrovgs/Shot
./gradlew executeScreenshotTests -Precord
Example Test
@get:Rule
val composeRule = createAndroidComposeRule<AccountDeletionActivity>()
@Test
fun accountDeletedScreenBodyScreenshot() {
composeRule.setContent { AccountDeletedScreenBody() }
compareScreenshot(composeRule)
}
./gradlew executeScreenshotTests
Example Output
6
Introducing Compose
to your team
Introducing Compose
Phased approach
– Do you have an existing app with custom UI
components?
– Recreate them in Compose!
– Provide a foundation to educate your team.
– Define standards & best practices.
Introducing Compose
medium.com/tinder/sparking-jetpack-compose-at-tinder-8f15d77eb4da
7
Conclusions
– We’ve adopted Jetpack Compose for all new Android
projects.
– Excellent official documentation & codelabs available.
– Good tooling and a growing list of third-party libraries
available.
– Recommend new starts prioritise Jetpack Compose over
XML.
Conclusions
EstiMate for Android is coming soon™
Questions?
Questions?

Compose In Practice

  • 1.
    © Instil Software2020 Compose Part 2 (the practice) Belfast Google Dev Group September 2022
  • 2.
  • 3.
    – Short historytour of Android UI Development. – Examples of using Jetpack Compose in across multiple apps. – Focus on Navigation, Accessibility & Testing. – Sneak peak at an upcoming Instil app! AGENDA
  • 4.
  • 5.
    The classic -findViewById XML layout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> id used by Activity
  • 6.
    The classic -findViewById Activity private lateinit var textView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById<TextView?>(R.id.textView).apply { text = "Hello GDE" textSize = 42f setTextColor(Color.CYAN) } }
  • 8.
  • 9.
  • 10.
    class DemoActivity :AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityDemoBinding> (this,R.layout.activity_demo) binding.vm = DemoViewModel() } } Databinding Activity XML layout file setting vm on XML
  • 11.
    class DemoViewModel { valtext = ObservableField("Data binding works!") } ViewModel
  • 12.
    The shiny newthing, Jetpack Compose!
  • 13.
  • 14.
    … but startingfrom scratch?
  • 15.
  • 19.
  • 21.
  • 22.
    Previews that work! @Composable @Preview(uiMode= UI_MODE_NIGHT_YES) fun CarouselScreenDarkPreview() { EstiMateTheme { CarouselScreen( {}, { CardSelection.Nuclear }, CardSelection.Nuclear ) } }
  • 23.
    Card( backgroundColor = instilGreen, shape= RoundedCornerShape(size = 16.dp), elevation = 8.dp, modifier = modifier .padding(8.dp) .width(120.dp) .height(150.dp) .clickable { onSelected(selection) }, ) { CardContent( selection = selection, fontSize = fontSize ) } EstiMateCard default modifier material component
  • 24.
    Box(contentAlignment = Alignment.Center){ when (selection) { is CoffeeBreak -> CoffeeImage() is Nuclear -> NuclearImage() else -> SelectionText(selection, fontSize) } } CardContent
  • 25.
  • 26.
    open fun getPlayers(teamName:String): Flow<List<Player>> = callbackFlow { firebaseService.observePlayersIn(teamName) { players -> trySend(players) } awaitClose { channel.close() } } PlayerService calling to Firebase posting a fresh list of players
  • 27.
    val players: StateFlow<List<Player>>= playerService.getPlayers() .stateIn( scope = viewModelScope, started = SharingStarted.Lazily, initialValue = emptyList() ) ViewModel called to PlayerService cancels the work once vm cleared
  • 28.
    val votedPlayers: List<Player>by viewModel.players.collectAsState() ... Composable calling to ViewModel LazyColumn(modifier = Modifier.fillMaxWidth()) { items(votedPlayers.size) { index -> UserVoteItem(votedPlayers[index]) } } composable per list item listOf( Player(CardSelection.Five, "Kelvin"), Player(CardSelection.Three, "Garth") )
  • 30.
    – Started in2016. – MVP pattern through use of interfaces. – Jetpack Compose introduced as we took ownership of development. – Shipped 2 new UI driven features with Jetpack Compose. Vypr Android App
  • 31.
  • 33.
    Using Jetpack Composein XML Views <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="match_parent" /> reference id
  • 34.
    Using Jetpack Composein XML Views private var _binding: FragmentSteersBinding? = null private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentSteersBinding.inflate(inflater, container, false) val view = binding.root binding.composeView.apply { setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) setContent { VyprTheme { SteersListView() } } } return view } compose_view xml element our new composable
  • 35.
    Using XML viewsin Jetpack Compose AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> AspectRatioImageView(context).apply { load(steer.previewImageUrl) setOnClickListener { onSteerClicked(steer) } } } ) legacy view with lots of scary
  • 37.
    – RecyclerView &adapter complexity removed. – Jetpack Compose views driven by lifecycle aware ViewModel. – More testable implementation. – We fixed the bug! Steers List Interop example
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
    – NavController -central API for stateful navigation. – NavHost - links NavController with a navigation graph. – Each Composable screen is known as a route. Jetpack Navigation Now supporting Compose
  • 44.
    Compose Navigation! NavHost(navController =navController, startDestination = "profile") { composable("profile") { Profile(/*...*/) } composable("friendslist") { FriendsList(/*...*/) } /*...*/ }
  • 45.
    Navigating to anotherRoute navController.navigate("friendslist")
  • 46.
    Compose Navigation With Arguments NavHost(startDestination= "profile/{userId}") { ... composable( "profile/{userId}", arguments = listOf(navArgument("userId") { type = NavType.StringType }) ) {...} } ... navController.navigate("profile/user1234")
  • 48.
  • 49.
    A KSP librarythat processes annotations and generates code that uses Official Jetpack Compose Navigation under the hood. It hides the complex, non-type-safe and boilerplate code you would have to write otherwise. Rafael Costa github.com/raamcosta/compose-destinations
  • 50.
    Adding a Destination @Destination(start= true) @Composable fun LoginRoute( destinationsNavigator: DestinationsNavigator ) { LoginScreen( ... ) } tag composable for generation provided for nav
  • 51.
    Generated NavGraph object NavGraphs{ val root = NavGraph( route = "root", startRoute = LoginRouteDestination, destinations = listOf( CarouselRouteDestination, LoginRouteDestination, ResultsRouteDestination, SelectionRouteDestination ) ) } each annotated composable labelled start
  • 52.
    Type safe navigation onLoginClicked= { destinationsNavigator.navigate(SelectionRouteDestination) },
  • 53.
    Type safe navigationwith args onCardSelected = { cardSelection -> destinationsNavigator.navigate( CarouselRouteDestination(selection = cardSelection) ) })
  • 54.
    Custom Serializer @NavTypeSerializer class CardSelectionSerializer: DestinationsNavTypeSerializer<CardSelection> { override fun fromRouteString(routeStr: String): CardSelection { return CardSelection.fromString(routeStr) } override fun toRouteString(value: CardSelection): String { return value.name } }
  • 55.
    Under the hood objectLoginRouteDestination : DirectionDestination { operator fun invoke() = this @get:RestrictTo(RestrictTo.Scope.SUBCLASSES) override val baseRoute = "login_route" override val route = baseRoute @Composable override fun DestinationScope<Unit>.Content( dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<Unit>.() -> Unit ) { LoginRoute( destinationsNavigator = destinationsNavigator ) } } our composable
  • 56.
  • 57.
  • 58.
    “Mobile accessibility” refersto making websites and applications more accessible to people with disabilities when they are using mobile phones and other devices. Shawn Lawton Henry w3.org/WAI/standards-guidelines/mobile/
  • 59.
    Android Accessibility – SwitchAccess: interact with switches instead of the touchscreen. – BrailleBack: refreshable Braille display to an Android device over Bluetooth. – Voice Access: control an Android device with spoken commands. – TalkBack: spoken feedback for UI interactions. What options are baked into the OS?
  • 60.
  • 61.
    An essential toolfor every Android team
  • 63.
  • 64.
    Play Store Testing- Example Issue
  • 65.
    Screen Reader issueconfirmed no context of dialog
  • 66.
    Fixing the Dialog valaccountDeletionDialogAccessibilityLabel = stringResource( id = R.string.accessibility_account_deletion_delete_dialog ) AlertDialog( ... modifier = Modifier.semantics(mergeDescendants = true) { contentDescription = accountDeletionDialogAccessibilityLabel } )
  • 67.
  • 68.
  • 69.
    Screen Sizes &Resolution
  • 70.
    UI Mockups – Discusswhat the UI toolkit can do when size is constrained. Compose is good at scaling text! – Agree how to handle view scaling. – Agree copy for accessibility labelling. – Collaborate with designers & product owners.
  • 71.
  • 72.
    Compose Semantics Semantics, asthe name implies, give meaning to a piece of UI. In this context, a "piece of UI" (or element) can mean anything from a single composable to a full screen. The semantics tree is generated alongside the UI hierarchy, and describes it.
  • 73.
    Example Button Button( modifier =Modifier.semantics { contentDescription = "Add to favorites" } ) individual ui elements make up a button easier to find
  • 74.
    Test Setup @get:Rule val composeTestRule= createAndroidComposeRule<VyPopsActivity>() @Before fun beforeEachTest() { composeTestRule.setContent { VyprTheme { VyPopsLandingScreen(EmptyDestinationsNavigator) } } }
  • 75.
    Finders Select one ormore elements (nodes) to assert or act on composeTestRule .onNodeWithContentDescription("Close Button") composeTestRule .onNodeWithText("What happens next")
  • 76.
    Finders - DebugLogging Node #1 at (l=0.0, t=54.0, r=720.0, b=1436.0)px |-Node #2 at (l=70.0, t=54.0, r=650.0, b=1436.0)px ContentDescription = '[VyPops Permissions Page]' |-Node #3 at (l=70.0, t=75.0, r=112.0, b=117.0)px | Role = 'Button' | Focused = 'false' | ContentDescription = '[Close Button]' | Actions = [OnClick] | MergeDescendants = 'true' |-Node #6 at (l=229.0, t=194.0, r=492.0, b=303.0)px | ContentDescription = '[Vypr Logo]' | Role = 'Image' |-Node #7 at (l=91.0, t=687.0, r=133.0, b=729.0)px | ContentDescription = '[Record Audio Tick]' | Role = 'Image' |-Node #8 at (l=147.0, t=684.0, r=615.0, b=731.0)px | Text = '[Microphone access granted]' | Actions = [GetTextLayoutResult] |-Node #9 at (l=125.0, t=762.0, r=167.0, b=804.0)px | ContentDescription = '[Camera Tick]' | Role = 'Image' |-Node #10 at (l=181.0, t=759.0, r=582.0, b=806.0)px | Text = '[Camera access granted]' | Actions = [GetTextLayoutResult] |-Node #11 at (l=84.0, t=1275.0, r=636.0, b=1366.0)px Text = '[VyPops needs access to both your camera and microphone.]' Actions = [GetTextLayoutResult] Node #1 at (l=0.0, t=54.0, r=720.0, b=1436.0)px |-Node #2 at (l=70.0, t=54.0, r=650.0, b=1436.0)px ContentDescription = '[VyPops Permissions Page]' |-Node #3 at (l=70.0, t=75.0, r=112.0, b=117.0)px | Role = 'Button' | Focused = 'false' | Actions = [OnClick] | MergeDescendants = 'true' | |-Node #5 at (l=70.0, t=75.0, r=112.0, b=117.0)px | ContentDescription = '[Close Button]' | Role = 'Image' |-Node #6 at (l=229.0, t=194.0, r=492.0, b=303.0)px | ContentDescription = '[Vypr Logo]' | Role = 'Image' |-Node #7 at (l=91.0, t=687.0, r=133.0, b=729.0)px | ContentDescription = '[Record Audio Tick]' | Role = 'Image' |-Node #8 at (l=147.0, t=684.0, r=615.0, b=731.0)px | Text = '[Microphone access granted]' | Actions = [GetTextLayoutResult] |-Node #9 at (l=125.0, t=762.0, r=167.0, b=804.0)px | ContentDescription = '[Camera Tick]' | Role = 'Image' |-Node #10 at (l=181.0, t=759.0, r=582.0, b=806.0)px | Text = '[Camera access granted]' | Actions = [GetTextLayoutResult] |-Node #11 at (l=84.0, t=1275.0, r=636.0, b=1366.0)px Text = '[VyPops needs access to both your camera and microphone.]' Actions = [GetTextLayoutResult]
  • 77.
    Assertions Verify elements existor have certain attributes composeTestRule .onNodeWithContentDescription("Login Button") .assertIsEnabled() composeTestRule .onNodeWithText("What happens next") .assertIsDisplayed()
  • 78.
    Simulate user inputor gestures Actions composeTestRule .onNodeWithContentDescription("Close Button") .performClick() ... .performTouchInput { swipeLeft() }
  • 79.
  • 80.
    Shot is aGradle plugin and a core android library thought to run screenshot tests for Android. Pedro Gómez github.com/pedrovgs/Shot
  • 81.
    ./gradlew executeScreenshotTests -Precord ExampleTest @get:Rule val composeRule = createAndroidComposeRule<AccountDeletionActivity>() @Test fun accountDeletedScreenBodyScreenshot() { composeRule.setContent { AccountDeletedScreenBody() } compareScreenshot(composeRule) }
  • 82.
  • 83.
  • 86.
    Introducing Compose Phased approach –Do you have an existing app with custom UI components? – Recreate them in Compose! – Provide a foundation to educate your team. – Define standards & best practices.
  • 87.
  • 88.
  • 89.
    – We’ve adoptedJetpack Compose for all new Android projects. – Excellent official documentation & codelabs available. – Good tooling and a growing list of third-party libraries available. – Recommend new starts prioritise Jetpack Compose over XML. Conclusions
  • 90.
    EstiMate for Androidis coming soon™
  • 91.