Android Summit 2020: Learn Jetpack Compose By Example

E8da8d13d06ca69dbe019ecad71ed2a4?s=47 vinaygaba
October 09, 2020

Android Summit 2020: Learn Jetpack Compose By Example

E8da8d13d06ca69dbe019ecad71ed2a4?s=128

vinaygaba

October 09, 2020
Tweet

Transcript

  1. Learn Jetpack Compose by Example Vinay Gaba @vinaygaba Join the

    conversation in #session-chat-d2-s2-t1
  2. Jetpack Compose / jet·pak kuhm·powz / noun Jetpack Compose is

    a declarative & modern toolkit for building native Android UI. It simplifies and accelerates UI development on Android.
  3. Why do we need Compose?

  4. Why do we need Compose? UI Toolkit is tied to

    the OS State Management is tricky Lots of context switching Simple things still require a lot of code
  5. Why do we need Compose? UI Toolkit is tied to

    the OS State Management is tricky Lots of context switching Simple things still require a lot of code
  6. Why do we need Compose? UI Toolkit is tied to

    the OS State Management is tricky Lots of context switching Simple things still require a lot of code
  7. Why do we need Compose? UI Toolkit is tied to

    the OS State Management is tricky Lots of context switching Simple things still require a lot of code
  8. Why do we need Compose? UI Toolkit is tied to

    the OS State Management is tricky Lots of context switching Simple things still require a lot of code
  9. Disclaimer

  10. Disclaimer Examples are based on 1.0.0-alpha03 Compose is in alpha

    so API’s can still change Beta release expected in the next few months
  11. Disclaimer Examples are based on 1.0.0-alpha03 Compose is in alpha

    so API’s can still change Beta release expected in the next few months
  12. Disclaimer Examples are based on 1.0.0-alpha03 Compose is in alpha

    so API’s can still change Beta release expected in the next few months
  13. Disclaimer Examples are based on 1.0.0-alpha03 Compose is in alpha

    so API’s can still change Beta release expected in the next few months
  14. Examples

  15. Hello World

  16. class HelloWorldActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    } }
  17. class HelloWorldActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent { } } }
  18. fun ComponentActivity.setContent( recomposer: Recomposer = Recomposer.current(), content: @Composable () "->

    Unit ): Composition { "// Some magic ✨ }
  19. class HelloWorldActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent { } } }
  20. class HelloWorldActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent { Text(text = "Hello World") } } }
  21. class HelloWorldActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent { Text(text = "Hello World") } } }
  22. class HelloWorldActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent { Text(text = "Hello World") } } }
  23. @Composable fun CustomTextComponent() { Text(text = "Hello World") }

  24. @Composable fun CustomTextComponent(displayText: String) { Text( text = displayText, style

    = TextStyle( fontSize = 18.sp, fontFamily = FontFamily.Monospace ) ) }
  25. @Composable fun CustomTextComponent(displayText: String) { Text( text = displayText, style

    = TextStyle( fontSize = 18.sp, fontFamily = FontFamily.Monospace ) ) }
  26. @Composable fun CustomTextComponent(displayText: String) { Text( text = displayText, style

    = TextStyle( fontSize = 18.sp, fontFamily = FontFamily.Monospace ) ) } @Preview @Composable fun CustomTextComponentPreview() { CustomTextComponent("Hello World") }
  27. None
  28. None
  29. Display Image

  30. @Composable fun DrawableImage() { }

  31. @Composable fun DrawableImage() { val image = loadImageResource(R.drawable.landscape) }

  32. @Composable fun DrawableImage() { val image = loadImageResource(R.drawable.landscape) image.resource.resource"?.let {

    Image(asset = it, modifier = Modifier.preferredSize(200.dp)) } } Modifiers are your best friends!
  33. @Composable fun DrawableImage(@DrawableRes resId: Int) { val image = loadImageResource(resId)

    image.resource.resource"?.let { Image( asset = it, modifier = Modifier.preferredSize(200.dp) ) } }
  34. @Composable fun DrawableImage(@DrawableRes resId: Int) { val image = loadImageResource(resId)

    image.resource.resource"?.let { Image( asset = it, modifier = Modifier.preferredSize(200.dp) ) } }
  35. Modifiers

  36. Text( text = "Hello", modifier = Modifier.padding(16.dp) )

  37. Text( text = "Hello", modifier = Modifier.padding(16.dp) .background(color = Color.Red)

    )
  38. Text( text = "Hello", modifier = Modifier.padding(16.dp) .background(color = Color.Red)

    )
  39. Text( text = "Hello", modifier = Modifier.padding(16.dp) .background(color = Color.Red)

    ) Hello
  40. Text( text = "Hello", modifier = Modifier.background(color = Color.Red) .padding(16.dp)

    )
  41. Text( text = "Hello", modifier = Modifier.background(color = Color.Red) .padding(16.dp)

    )
  42. Text( text = "Hello", modifier = Modifier.background(color = Color.Red) .padding(16.dp)

    ) Hello The order of a Modifier has an impact on the behavior
  43. Alert Dialog

  44. Jetpack Compose / jet·pak kuhm·powz / noun Jetpack Compose is

    a declarative & modern toolkit for building native Android UI. It simplifies and accelerates UI development on Android.
  45. How What vs

  46. How What

  47. How What // Classic Android val alertDialog = AlertDialog.Builder(context) .setTitle("Android

    Summit!”) .setMessage("Isn't this conference amazing?") // Somewhere else in code if (some_condition_is_met()) { alertDialog.show() } // Somewhere else in code if (some_other_condition_is_met()) { alertDialog.dismiss() }
  48. How What // Classic Android val alertDialog = AlertDialog.Builder(context) .setTitle("Android

    Summit!”) .setMessage("Isn't this conference amazing?") // Somewhere else in code if (some_condition_is_met()) { alertDialog.show() } // Somewhere else in code if (some_other_condition_is_met()) { alertDialog.dismiss() } @Composable fun AlertDialogComponent() { }
  49. How What // Classic Android val alertDialog = AlertDialog.Builder(context) .setTitle("Android

    Summit!”) .setMessage("Isn't this conference amazing?") // Somewhere else in code if (some_condition_is_met()) { alertDialog.show() } // Somewhere else in code if (some_other_condition_is_met()) { alertDialog.dismiss() } @Composable fun AlertDialogComponent() { if (some_condition_is_met()) { } }
  50. How What // Classic Android val alertDialog = AlertDialog.Builder(context) .setTitle("Android

    Summit!”) .setMessage("Isn't this conference amazing?") // Somewhere else in code if (some_condition_is_met()) { alertDialog.show() } // Somewhere else in code if (some_other_condition_is_met()) { alertDialog.dismiss() } @Composable fun AlertDialogComponent() { if (some_condition_is_met()) { AlertDialog( title = { Text("Android Summit!") }, text = { Text(text = "Isn't this amazing?") } ) } }
  51. State

  52. @Composable fun AlertDialogComponent() { var showPopup by remember { mutableStateOf(false)

    } }
  53. @Composable fun AlertDialogComponent() { var showPopup by remember { mutableStateOf(false)

    } Button(onClick = { showPopup = true }) { Text(text = "Click Me") } }
  54. @Composable fun AlertDialogComponent() { var showPopup by remember { mutableStateOf(false)

    } Button(onClick = { showPopup = true }) { Text(text = "Click Me") } if (showPopup) { AlertDialog( onCloseRequest = { showPopup = false }, text = { Text("Congratulations! You just clicked the text successfully") }, confirmButton = { Button( onClick = onPopupDismissed ) { Text(text = "Ok") } } ) } }
  55. @Composable fun AlertDialogComponent() { var showPopup by remember { mutableStateOf(false)

    } Button(onClick = { showPopup = true }) { Text(text = "Click Me") } if (showPopup) { AlertDialog( onCloseRequest = { showPopup = false }, text = { Text("Congratulations! You just clicked the text successfully") }, confirmButton = { Button( onClick = onPopupDismissed ) { Text(text = "Ok") } } ) } }
  56. @Composable fun AlertDialogComponent() { var showPopup by remember { mutableStateOf(false)

    } if (!showPopup) { Button(onClick = { showPopup = true }) { Text(text = "Click Me") } } else { AlertDialog( onCloseRequest = { showPopup = false }, text = { Text("Congratulations! You just clicked the text successfully") }, confirmButton = { Button( onClick = onPopupDismissed ) { Text(text = "Ok") } } ) } }
  57. Recomposition

  58. Recompose / re·kuhm·powz / verb In an imperative UI model,

    to change a widget, you call a setter on the widget to change its internal state. In Compose, you call the composable function again with new data. Doing so causes the function to be recomposed--the widgets emitted by the function are redrawn, if necessary, with new data. The Compose framework can intelligently recompose only the components that changed.
  59. (user: User) (name: String) (age: Int) (user: User) [counter: Int]

    (user: User) (imageURL: String) [scale: Float] (address: String)
  60. (user: User) (name: String) (age: Int) (user: User) [counter: Int]

    (user: User) (imageURL: String) [scale: Float] (address: String)
  61. (user: User) (name: String) (age: Int) (user: User) [counter: Int]

    (user: User) (imageURL: String) [scale: Float] (address: String)
  62. (user: User) (name: String) (age: Int) (user: User) [counter: Int]

    (user: User) (imageURL: String) [scale: Float] (address: String)
  63. (user: User) (name: String) (age: Int) (user: User) [counter: Int]

    (user: User) (imageURL: String) [scale: Float] (address: String)
  64. (user: User) (name: String) (age: Int) (user: User) [counter: Int]

    (user: User) (imageURL: String) [scale: Float] (address: String)
  65. Rules of Recomposition

  66. Rules of Recomposition Some composable functions could be skipped Composable

    functions can be called frequently Composable functions can execute in any order Composable functions can run in parallel
  67. @Composable fun ParentComposable() { "// Don’t write logic that always

    depends "// on the execution of all the composable Child1Composable() Child2Composable() Child3Composable() }
  68. Rules of Recomposition Some composable functions could be skipped Composable

    functions can be called frequently Composable functions can execute in any order Composable functions can run in parallel
  69. Rules of Recomposition Some composable functions could be skipped Composable

    functions can be called frequently Composable functions can execute in any order Composable functions can run in parallel
  70. "// When this component is called from inside an animation,

    "// it will be called on every frame. @Composable fun ComponentCalledFromAnimation() { "// expensiveOperation takes 2 seconds to run val result = expensiveOperation() Text(result.message) }
  71. "// When this component is called from inside an animation,

    "// it will be called on every frame. @Composable fun ComponentCalledFromAnimation() { "// expensiveOperation takes 2 seconds to run val result = expensiveOperation() Text(result.message) } launchInComposition
  72. Rules of Recomposition Some composable functions could be skipped Composable

    functions can be called frequently Composable functions can execute in any order Composable functions can run in parallel
  73. @Composable fun ParentComposable() { "// Can be called in any

    order Child1Composable() Child2Composable() Child3Composable() }
  74. Rules of Recomposition Composable functions could be skipped Composable functions

    can be called frequently Composable functions can execute in any order Composable functions can run in parallel
  75. Simple Layouts

  76. None
  77. Row

  78. Row 1 2

  79. 1 Column 1 Row 2

  80. @Composable fun ImageWithTitleSubtitleComponent() { }

  81. @Composable fun ImageWithTitleSubtitleComponent() { Row() { Column() { } }

    }
  82. @Composable fun ImageWithTitleSubtitleComponent() { Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Column(modifier =

    Modifier.padding(start = 16.dp)) { } } }
  83. @Composable fun ImageWithTitleSubtitleComponent() { Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) { DrawableImage(R.drawable.landscape) Column(modifier

    = Modifier.padding(start = 16.dp)) { CustomTextComponent(displayText = "Title") CustomTextComponent(displayText = "Subtitle") } } }
  84. @Composable fun ImageWithTitleSubtitleComponent( title: String, subtitle: String, imageUrl: String )

    { Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) { NetworkImage(imageUrl) Column(modifier = Modifier.padding(start = 16.dp)) { CustomTextComponent(displayText = title) CustomTextComponent(displayText = subtitle) } } }
  85. @Composable fun ImageWithTitleSubtitleComponent( title: String, subtitle: String, imageUrl: String )

    { Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) { NetworkImage(imageUrl) Column(modifier = Modifier.padding(start = 16.dp)) { CustomTextComponent(displayText = title) CustomTextComponent(displayText = subtitle) } } }
  86. None
  87. Display List

  88. None
  89. None
  90. @Composable fun ListComponent(superheroList: List<Person>) { }

  91. @Composable fun ListComponent(superheroList: List<Person>) { ScrollableColumn { for(person in superheroList)

    { SimpleRowComponent( person.name, person.age, person.profilePictureUrl ) } } }
  92. www.JetpackCompose.app

  93. @Composable fun ListComponent(superheroList: List<Person>) { }

  94. @Composable fun ListComponent(superheroList: List<Person>) { LazyColumnFor(items = superheroList) { person

    "-> } }
  95. @Composable fun ListComponent(superheroList: List<Person>) { LazyColumnFor(items = superheroList) { person

    "-> SimpleRowComponent( person.name, person.age, person.profilePictureUrl ) } }
  96. @Composable fun ListComponent(superheroList: List<Person>) { LazyColumnFor(items = superheroList) { person

    "-> SimpleRowComponent( person.name, person.age, person.profilePictureUrl ) } }
  97. Thank You Droid God, For this new day. I will

    rest in your promises(coroutines) of a world free of fragments. Guide me with compile-time checks and help me in every @SuppressWarnings that I add.
  98. Click Gesture

  99. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String )

    { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp), shape = RoundedCornerShape(4.dp) ) { ""... ""... ""... } }
  100. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String, viewModel:

    SuperheroViewModel ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) .cl shape = RoundedCornerShape(4.dp) ) { .... .... } }
  101. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String, viewModel:

    SuperheroViewModel ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) .cl shape = RoundedCornerShape(4.dp) ) { .... .... } } clip clickable clipToBounds
  102. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String, viewModel:

    SuperheroViewModel ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) + .clickable { viewModel.updateSelectedSuperhero() }, shape = RoundedCornerShape(4.dp) ) { .... .... } }
  103. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String, viewModel:

    SuperheroViewModel ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) + .clickable { viewModel.updateSelectedSuperhero() }, shape = RoundedCornerShape(4.dp) ) { .... .... } }
  104. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String, onClick:

    () "-> Unit ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) .clickable { onClick() }, shape = RoundedCornerShape(4.dp) ) { .... .... } }
  105. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String, onClick:

    () "-> Unit ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) .clickable { onClick() }, shape = RoundedCornerShape(4.dp) ) { .... .... } }
  106. Pinch-to-Zoom & Drag

  107. @Composable fun ZoomableImageComponent(imageUrl: String) { }

  108. @Composable fun ZoomableImageComponent(imageUrl: String) { var scale by state {

    1f } var panOffset by state { Offset(0f, 0f) } }
  109. @Composable fun ZoomableImageComponent(imageUrl: String) { var scale by state {

    1f } var panOffset by state { Offset(0f, 0f) } Box(gravity = Alignment.Center) { NetworkImage( imageUrl = imageUrl, modifier = Modifier.fillMaxSize() ) } }
  110. @Composable fun ZoomableImageComponent(imageUrl: String) { var scale by state {

    1f } var panOffset by state { Offset(0f, 0f) } Box( gravity = Alignment.Center, modifier = Modifier.zoomable(onZoomDelta = { scale *= it }) ) { NetworkImage( imageUrl = imageUrl, modifier = Modifier.fillMaxSize().drawLayer( scaleX = scale, scaleY = scale ) ) } }
  111. @Composable fun ZoomableImageComponent(imageUrl: String) { var scale by state {

    1f } var panOffset by state { Offset(0f, 0f) } Box( gravity = Alignment.Center, modifier = Modifier.zoomable(onZoomDelta = { scale *= it }).rawDragGestureFilter( object : DragObserver { override fun onDrag(dragDistance: Offset): Offset { panOffset = panOffset.plus(dragDistance) return super.onDrag(dragDistance) } }) ) { NetworkImage( imageUrl = imageUrl, modifier = Modifier.fillMaxSize().drawLayer( scaleX = scale, scaleY = scale, translationX = panOffset.x, translationY = panOffset.y ) ) } }
  112. @Composable fun ZoomableImageComponent(imageUrl: String) { var scale by state {

    1f } var panOffset by state { Offset(0f, 0f) } Box( gravity = Alignment.Center, modifier = Modifier.zoomable(onZoomDelta = { scale *= it }).rawDragGestureFilter( object : DragObserver { override fun onDrag(dragDistance: Offset): Offset { panOffset = panOffset.plus(dragDistance) return super.onDrag(dragDistance) } }) ) { NetworkImage( imageUrl = imageUrl, modifier = Modifier.fillMaxSize().drawLayer( scaleX = scale, scaleY = scale, translationX = panOffset.x, translationY = panOffset.y ) ) } }
  113. None
  114. Compose in Classic Android

  115. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http:"//schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text_view" android:layout_width="wrap_content"

    android:layout_height="wrap_content" "/> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="wrap_content" android:layout_height="wrap_content" "/> "</LinearLayout> activity_compose_in_classic_android.xml
  116. <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http:"//schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text_view" android:layout_width="wrap_content"

    android:layout_height="wrap_content" "/> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="wrap_content" android:layout_height="wrap_content" "/> "</LinearLayout> activity_compose_in_classic_android.xml
  117. class ComposeInClassicAndroidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_compose_in_classic_android) } }
  118. class ComposeInClassicAndroidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_compose_in_classic_android) val composeView = findViewById(R.id.compose_view) composeView.setContent { SimpleRowComponent() } } }
  119. class ComposeInClassicAndroidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_compose_in_classic_android) val composeView = findViewById(R.id.compose_view) composeView.setContent { SimpleRowComponent() } } }
  120. Classic Android in Compose

  121. @Composable fun ClassAndroidInComposeComponent() { }

  122. @Composable fun ClassAndroidInComposeComponent() { val context = ContextAmbient.current val classicTextView

    = remember { TextView(context) } }
  123. @Composable fun ClassAndroidInComposeComponent() { val context = ContextAmbient.current val classicTextView

    = remember { TextView(context) } AndroidView(viewBlock = { classicTextView }) { view "-> "// view is inflated here. Do anything if your logic requires it } }
  124. ViewModel

  125. @Composable fun MoviesComponent() { val viewModel: MoviesViewModel = viewModel() "//

    or val viewModel: MoviesViewModel = viewModel(ViewModelProvider.Factory) }
  126. LiveData

  127. @Composable fun MoviesComponent() { val viewModel: MoviesViewModel = viewModel() }

  128. @Composable fun MoviesComponent() { val viewModel: MoviesViewModel = viewModel() val

    movieList = viewModel.movieListLiveData }
  129. @Composable fun MoviesComponent() { val viewModel: MoviesViewModel = viewModel() val

    movieList = viewModel.movieListLiveData.observeAsState() }
  130. @Composable fun MoviesComponent() { val viewModel: MoviesViewModel = viewModel() val

    movieList = viewModel.movieListLiveData.observeAsState() } ‘X’AsState()
  131. Testing

  132. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String )

    { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp), shape = RoundedCornerShape(4.dp) ) { ""... ""... } }
  133. @RunWith(JUnit4"::class) class SimpleRowComponentTest { }

  134. @RunWith(JUnit4"::class) class SimpleRowComponentTest { @get:Rule val composeTestRule = createComposeRule(disableTransitions =

    true) }
  135. @RunWith(JUnit4"::class) class SimpleRowComponentTest { @get:Rule val composeTestRule = createComposeRule(disableTransitions =

    true) @Before fun setUp() { composeTestRule.setContent { SimpleRowComponent( titleText = "Title", subtitleText = "Subtitle", imageUrl = "https:"//www.google.com/demo.jpg" ) } } }
  136. @RunWith(JUnit4"::class) class SimpleRowComponentTest { @get:Rule val composeTestRule = createComposeRule(disableTransitions =

    true) @Before fun setUp() { composeTestRule.setContent { SimpleRowComponent( titleText = "Title", subtitleText = "Subtitle", imageUrl = "https:"//www.google.com/demo.jpg" ) } } @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title") } }
  137. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title") }

  138. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithSubstring("Ti") }

  139. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithTag("TitleTag") }

  140. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithTag("TitleTag") } "// In the component

    @Composable fun SimpleRowComponent(""...) { Card( modifier = Modifier.testTag("TitleTag") ) { ""... } }
  141. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title") }

  142. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title").assertIsDisplayed() }

  143. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title").assertIsHidden() }

  144. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title").assertHasClickAction() }

  145. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title").assertHeightIsAtLeast(100.dp) }

  146. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title").performClick() }

  147. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title").performGesture { swipeDown() } }

  148. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithText("Title").assertIsDisplayed() }

  149. Resources

  150. Christmas came early"!!

  151. https:"//bit.ly/ComposeByExample

  152. https:"//www.JetpackCompose.app/

  153. https:"//www.JetpackCompose.app/FAQ

  154. https:"//www.JetpackCompose.app/Quick-Bites @vinaygaba

  155. https:"//github.com/airbnb/Showkase

  156. Learn Jetpack Compose by Example Vinay Gaba @vinaygaba https://bit.ly/ComposeByExample