Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Summit 2020: Learn Jetpack Compose By Example

vinaygaba
October 09, 2020

Android Summit 2020: Learn Jetpack Compose By Example

Over the course of the last few years Android development has gone through significant changes in how we structure our apps, the language we use for development, the tooling & libraries that help us speed up our development and the improvements in testing our apps. What had not changed in all these years is the Android UI toolkit. This changes with Jetpack Compose that aims to reimagine what Android UI development would look like using declarative programming principles. It is heavily influenced by existing web and mobile frameworks such as React, Litho, Vue & Flutter and would be a paradigm shift in Android UI development as we know it.

In this talk, we will take a deeper look at what declarative programming means and how we should think about it when building our apps. We will look into the main principles of Jetpack Compose and try to draw parallels with the “old Android way” of doing common tasks. Lastly, we will dive into various code examples and learn how to build layouts, manage state, write custom views, style our views, access resources and more, all using Jetpack Compose.

Companion code – https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example

vinaygaba

October 09, 2020
Tweet

More Decks by vinaygaba

Other Decks in Technology

Transcript

  1. 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.
  2. 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
  3. 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
  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. 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
  8. 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
  9. 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
  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. @Composable fun CustomTextComponent(displayText: String) { Text( text = displayText, style

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

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

    = TextStyle( fontSize = 18.sp, fontFamily = FontFamily.Monospace ) ) } @Preview @Composable fun CustomTextComponentPreview() { CustomTextComponent("Hello World") }
  14. @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!
  15. @Composable fun DrawableImage(@DrawableRes resId: Int) { val image = loadImageResource(resId)

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

    image.resource.resource"?.let { Image( asset = it, modifier = Modifier.preferredSize(200.dp) ) } }
  17. Text( text = "Hello", modifier = Modifier.background(color = Color.Red) .padding(16.dp)

    ) Hello The order of a Modifier has an impact on the behavior
  18. 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.
  19. 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() }
  20. 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() { }
  21. 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()) { } }
  22. 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?") } ) } }
  23. @Composable fun AlertDialogComponent() { var showPopup by remember { mutableStateOf(false)

    } Button(onClick = { showPopup = true }) { Text(text = "Click Me") } }
  24. @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") } } ) } }
  25. @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") } } ) } }
  26. @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") } } ) } }
  27. 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.
  28. (user: User) (name: String) (age: Int) (user: User) [counter: Int]

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

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

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

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

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

    (user: User) (imageURL: String) [scale: Float] (address: String)
  34. 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
  35. @Composable fun ParentComposable() { "// Don’t write logic that always

    depends "// on the execution of all the composable Child1Composable() Child2Composable() Child3Composable() }
  36. 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
  37. 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
  38. "// 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) }
  39. "// 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
  40. 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
  41. @Composable fun ParentComposable() { "// Can be called in any

    order Child1Composable() Child2Composable() Child3Composable() }
  42. 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
  43. Row

  44. @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") } } }
  45. @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) } } }
  46. @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) } } }
  47. @Composable fun ListComponent(superheroList: List<Person>) { ScrollableColumn { for(person in superheroList)

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

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

    "-> SimpleRowComponent( person.name, person.age, person.profilePictureUrl ) } }
  50. 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.
  51. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String )

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

    SuperheroViewModel ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) .cl shape = RoundedCornerShape(4.dp) ) { .... .... } }
  53. @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
  54. @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) ) { .... .... } }
  55. @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) ) { .... .... } }
  56. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String, onClick:

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

    () "-> Unit ) { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp) .clickable { onClick() }, shape = RoundedCornerShape(4.dp) ) { .... .... } }
  58. @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() ) } }
  59. @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 ) ) } }
  60. @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 ) ) } }
  61. @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 ) ) } }
  62. <?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
  63. <?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
  64. class ComposeInClassicAndroidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_compose_in_classic_android) } }
  65. 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() } } }
  66. 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() } } }
  67. @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 } }
  68. @Composable fun MoviesComponent() { val viewModel: MoviesViewModel = viewModel() "//

    or val viewModel: MoviesViewModel = viewModel(ViewModelProvider.Factory) }
  69. @Composable fun MoviesComponent() { val viewModel: MoviesViewModel = viewModel() val

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

    movieList = viewModel.movieListLiveData.observeAsState() } ‘X’AsState()
  71. @Composable fun SimpleRowComponent( titleText: String, subtitleText: String, imageUrl: String )

    { Card( modifier = Modifier.fillMaxWidth() .padding(8.dp), shape = RoundedCornerShape(4.dp) ) { ""... ""... } }
  72. @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" ) } } }
  73. @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") } }
  74. @Test fun check_if_card_is_displayed() { composeTestRule.onNodeWithTag("TitleTag") } "// In the component

    @Composable fun SimpleRowComponent(""...) { Card( modifier = Modifier.testTag("TitleTag") ) { ""... } }