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

Adopting Jetpack Compose in your Android app - DevFest 2022

Shreyas Patil
December 02, 2022

Adopting Jetpack Compose in your Android app - DevFest 2022

Shreyas Patil

December 02, 2022
Tweet

Other Decks in Technology

Transcript

  1. Adopting Jetpack Compose in your Android app Shreyas Patil Google

    Dev Expert - Android Android @ Paytm shreyaspatil.dev
  2. Who am I? 󰞦 • Senior Software Engineer - Android

    @ Paytm • Google Developers Experts - Android • Community Organizer @ Kotlin Mumbai • Develop Android and Web applications • Open source contributor & maintainer • Write blogs @ blog.shreyaspatil.dev
  3. What is Jetpack Compose?

  4. • Modern UI development toolkit What is Jetpack Compose?

  5. • Modern UI development toolkit • Fully built with Kotlin

    What is Jetpack Compose?
  6. • Modern UI development toolkit • Fully built with Kotlin

    • Simplifies and accelerates UI development What is Jetpack Compose?
  7. • Modern UI development toolkit • Fully built with Kotlin

    • Simplifies and accelerates UI development • Less code What is Jetpack Compose?
  8. • Modern UI development toolkit • Fully built with Kotlin

    • Simplifies and accelerates UI development • Less code • Intuitive Kotlin APIs What is Jetpack Compose?
  9. • Modern UI development toolkit • Fully built with Kotlin

    • Simplifies and accelerates UI development • Less code • Intuitive Kotlin APIs • Powerful API and tools What is Jetpack Compose?
  10. What is Jetpack Compose? Google Jetpack Compose v1.0.0 (July 2021)

  11. Why Jetpack Compose?

  12. Why Jetpack Compose?

  13. • Single language - Kotlin Why Jetpack Compose?

  14. • Single language - Kotlin • UI and operations in

    same place Why Jetpack Compose?
  15. • Single language - Kotlin • UI and operations in

    same place • Declarative UI. Reactive pattern. Why Jetpack Compose?
  16. • Single language - Kotlin • UI and operations in

    same place • Declarative UI. Reactive pattern. • Loosely coupled code of UI Why Jetpack Compose?
  17. View vs Jetpack Compose -/ MainActivity.kt class MainActivity: Activity() {

    override fun onCreate(...) { setContentView(R.layout.activity_main) } } -/ activity_main.xml <LinearLayout ...> <TextView ... android:text="Hello World" .> ./LinearLayout> -/ MainActivity.kt class MainActivity: Activity() { override fun onCreate(...) { setContent { Greeting() } } } @Composable fun Greeting() { Text("Hello World!") } With View UI With Jetpack Compose UI
  18. • Single language - Kotlin • UI and operations in

    same place • Declarative UI. Reactive pattern. • Loosely coupled code of UI • Scope for reusable UI components Why Jetpack Compose?
  19. • Single language - Kotlin • UI and operations in

    same place • Declarative UI. Reactive pattern. • Loosely coupled code of UI • Scope for reusable UI components • Better, testable, debuggable code for UI Why Jetpack Compose?
  20. • Single language - Kotlin • UI and operations in

    same place • Declarative UI. Reactive pattern. • Loosely coupled code of UI • Scope for reusable UI components • Better, testable, debuggable code for UI • Interoperable with Android View Why Jetpack Compose?
  21. Who is using Jetpack compose? See: developer.android.com/jetpack/compose/adopt

  22. How to adopt Jetpack Compose?

  23. 1. Migrate mindset and thinking first 2. Discuss and plan

    3. Finally, kickstart adoption and migrate 🚀 Steps for adoption
  24. Migrating from View mindset and Thinking in Jetpack Compose 🤔

  25. Migrating from View mindset and thinking in Compose • Bind

    Views • Access properties via getters • Set properties via setters • Listen View events via listeners • Compose components • Declarative way ◦ Compose component ◦ State IN ◦ Events OUT Imperative way in View Declarative way in Jetpack Compose
  26. Imperative vs Declarative lateinit var editText: editText fun demo() {

    ./ Set value editText.setText("Edited") ./ Get value val value = editText.text.toString() ./ Get real-time updates editText.doAfterTextChanged { val value = it.toString() } } @Composable fun Demo() { var text: String by remember { mutableStateOf("Initial Value") } TextField( value = text, onValueChange = { newValue .> text = newValue } ) } With old Imperative View UI With Declarative Jetpack Compose UI
  27. Declarative Paradigm Screen Sub content 1 Content Sub content 2

    Sub content 3 Data (as State) Event Reference: developer.android.com/jetpack/compose/mental-model
  28. @Composable fun CounterScreen() { var count by remember { mutableStateOf(0)

    } Counter(count, onCountIncrement = { count-+ }) } @Composable fun Counter( count: Int, onCountIncrement: () .> Unit ) { Text("Count = $count") Button(onClick = onCountIncrement) { Text("Increment +") } } Stateful components and Stateless components
  29. @Composable fun CounterScreen() { var count by remember { mutableStateOf(0)

    } Counter(count, onCountIncrement = { count.+ }) } @Composable fun Counter( count: Int, onCountIncrement: () .> Unit ) { Text("Count = $count") Button(onClick = onCountIncrement) { Text("Increment +") } } Stateful components and Stateless components
  30. @Composable fun CounterScreen() { var count by remember { mutableStateOf(0)

    } Counter(count, onCountIncrement = { count.+ }) } @Composable fun Counter( count: Int, onCountIncrement: () .> Unit ) { Text("Count = $count") Button(onClick = onCountIncrement) { Text("Increment +") } } Stateful components and Stateless components
  31. @Composable fun CounterScreen() { var count by remember { mutableStateOf(0)

    } Counter(count, onCountIncrement = { count.+ }) } @Composable fun Counter( count: Int, onCountIncrement: () .> Unit ) { Text("Count = $count") Button(onClick = onCountIncrement) { Text("Increment +") } } Thinking & Designing screen in Compose Screen Content State Event Sub-content 1 Sub-content 2 Content 1 Screen • Make content stateless. • Make screen stateful • Let the data flow unidirectional. • Screen will provide state for content. • Content will give events back to screen.
  32. 📐 Discuss and Plan

  33. Discuss and Plan • Find scope for replacement of component

    in the existing UI. • Start replacing the simplest and small components first in the existing UI. • Migrate screen fully into Jetpack Compose. • Design new components or screens in Compose.
  34. 🎢 Kickstarting adoption of Jetpack Compose

  35. 󰱢 Migrating existing screen in Jetpack Compose

  36. • Extract components into unit Composables. • Content from components.

    • Screen from content. Migrating existing Screen in Compose
  37. • Top App bar Components required for this screen

  38. • Top App bar • Search Field Components required for

    this screen
  39. • Top App bar • Search Field • Split Button

    Components required for this screen
  40. • Top App bar • Search Field • Split Button

    • List header Components required for this screen
  41. • Top App bar • Search Field • Split Button

    • List header • List initial Components required for this screen
  42. • Top App bar • Search Field • Split Button

    • List header • List initial • Contact Item Components required for this screen
  43. • Top App bar • Search Field • Split Button

    • List header • List initial • Contact Item • Item Scroller Components required for this screen
  44. • Column ◦ Column ▪ Top App bar ▪ Search

    Field ▪ Split Button ▪ List header ◦ Box ▪ LazyColumn(Contact Items) ▪ Item Scroller Contents of this screen
  45. @Composable fun ContactSearchScreen( viewModel: ContactSearchViewModel ) { val state by

    viewModel.state.collectAsState() ContactSearchContent(--.) } @Composable fun ContactSearchContent(...) {...} Assembling contents in Screen
  46. class ContactSearchActivity: ComponentActivity() { override fun onCreate(...) { ... setContent

    { ContactSearchScreen(viewModel) } } } @Composable fun ContactSearchScreen(viewModel: ContactSearchViewModel) { val state by viewModel.state.collectAsState() ContactSearchContent(...) } Render Screen in Activity
  47. 󰱢 Using Compose in Fragment

  48. class NewFeatureFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater,

    container: ViewGroup?, savedInstanceState: Bundle? ): View = ComposeView(requireContext()).apply { setContent { NewFeatureScreen() } } } Using Composable in Fragment
  49. class NewFeatureFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater,

    container: ViewGroup?, savedInstanceState: Bundle? ): View = ComposeView(requireContext()).apply { setContent { NewFeatureScreen() } } } Using Composable in Fragment
  50. 󰱢 Theme interoperability XML < > Compose

  51. @Composable fun App() { MaterialTheme(...) { AppContent() } } Material

    Theme in Compose But what if your app already have Material Theme defined in styles.xml? 🤨
  52. Material Theme Compose Adapter Source: https://github.com/material-components/material-components-android-compose-theme-adapter

  53. // Add dependency in Gradle dependencies { // Compatible with

    Compose Material, includes MdcTheme implementation "com.google.android.material:compose-theme-adapter:version" // Compatible with Compose Material 3, includes Mdc3Theme implementation "com.google.android.material:compose-theme-adapter-3:version" } Material Theme Compose Adapter
  54. // Theme defined in your app’s XML <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight">

    <!-- Material 2 color attributes --> <item name="colorPrimary">@color/purple_500</item> <item name="colorSecondary">@color/green_200</item> <!-- Material 2 type attributes--> <item name="textAppearanceBody1">@style/TextAppearance.MyApp.Body1</item> <item name="textAppearanceBody2">@style/TextAppearance.MyApp.Body2</item> </style> Material Theme Compose Adapter
  55. // Use in your Composable @Composable fun MyApp() { MdcTheme

    { // MaterialTheme.colors, MaterialTheme.typography, // MaterialTheme.shapes will now contain copies of // the Context's theme } } Material Theme Compose Adapter
  56. // Add dependency of AppCompat Theme Adapter by Accompanist dependencies

    { implementation "com.google.accompanist:accompanist-themeadapter-appcompat:version" } // Use in composable @Composable fun MyApp() { AppCompatTheme { // MaterialTheme.colors, MaterialTheme.shapes, MaterialTheme.typography // will now contain copies of the context's theme } } Using AppCompat theme? 🤨
  57. 󰱢 <ComposeView> Compose in Existing UI

  58. Example: Search Field powered by Jetpack Compose in Paytm home

    screen. Using Compose in Existing UI
  59. ./ 1. Place ComposeView in XML layout <LinearLayout ...> <androidx.compose.ui.platform.ComposeView

    android:id="@+id/header_search_view" ... .> ... ./LinearLayout> Using Compose in Existing UI
  60. ./ 2. Retrieve ComposeView in Activity val headerSearchView = findViewById(R.id.header_search_view)

    Using Compose in Existing UI
  61. ./ 3. Compose your UI with ComposeView val headerSearchView =

    findViewById(R.id.header_search_view) headerSearchView.setContent { PaytmHeaderSearch(...) } Using Compose in Existing UI
  62. 󰱢 <AbstractComposeView> Composable as Custom View

  63. Example: Powering bottom layout of Chat screen in Paytm Component

    as Custom View
  64. Example: Powering bottom layout of Chat screen in Paytm Component

    as Custom View
  65. ./ 1. Design a component in Jetpack Compose @Composable fun

    ChatBottomContent(...) { } Component as Custom View
  66. ./ 2. Create a View extending AbstractComposeView class ChatBottomContentView @JvmOverloads

    constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { Component as Custom View
  67. ./ 3. Override Content() function and compose UI class ChatBottomContentView

    @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var message by mutableStateOf<String>("") ... @Composable override fun Content() { PaytmTheme { ChatBottomContent(message, ...) } } } Component as Custom View
  68. ./ 4. Place custom View in XML hierarchy <LinearLayout ...>

    ... <com.paytm.chat.ui.ChatBottomContentView android:id="@+id/chat_bottom_content" ... .> ./LinearLayout> Component as Custom View
  69. ./ 5. Access View in Activity/Fragment val bottomContentView = findViewById<ChatBottomContentView>(

    R.id.chat_bottom_content ) ./ Access properties bottomContentView.message = "Hi there!" ./ Access events bottomContentView.onSendClick { sendMessage(bottomContentView.message) } Component as Custom View
  70. 󰱢 <AndroidView> Custom View as Composable

  71. @Composable fun <T : View> AndroidView( factory: (Context) .> T,

    modifier: Modifier = Modifier, update: (T) .> Unit = NoOpUpdate ): Unit Custom View as Composable
  72. class MyCustomView: View() {--.} @Composable fun MyCustomView() { val selectedItem

    by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context .> CustomView(context).apply { setOnItemChangeListener { item .> selectedItem = item } } }, update = { view .> view.selectedItem = selectedItem } ) }
  73. class MyCustomView: View() {...} @Composable fun MyCustomView() { val selectedItem

    by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context .> CustomView(context).apply { setOnItemChangeListener { item .> selectedItem = item } } }, update = { view .> view.selectedItem = selectedItem } ) }
  74. class MyCustomView: View() {...} @Composable fun MyCustomView() { val selectedItem

    by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context .> CustomView(context).apply { setOnItemChangeListener { item .> selectedItem = item } } }, update = { view .> view.selectedItem = selectedItem } ) }
  75. class MyCustomView: View() {...} @Composable fun MyCustomView() { val selectedItem

    by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context .> CustomView(context).apply { setOnItemChangeListener { item .> selectedItem = item } } }, update = { view .> view.selectedItem = selectedItem } ) }
  76. class MyCustomView: View() {...} @Composable fun MyCustomView() { val selectedItem

    by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context .> CustomView(context).apply { setOnItemChangeListener { item .> selectedItem = item } } }, update = { view .> view.selectedItem = selectedItem } ) }
  77. class MyCustomView: View() {...} @Composable fun MyCustomView() { val selectedItem

    by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context .> CustomView(context).apply { setOnItemChangeListener { item .> selectedItem = item } } }, update = { view .> view.selectedItem = selectedItem } ) }
  78. class MyCustomView: View() {...} @Composable fun MyCustomView() { val selectedItem

    by remember { mutableStateOf(0) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { context .> CustomView(context).apply { setOnItemChangeListener { item .> selectedItem = item } } }, update = { view .> view.selectedItem = selectedItem } ) }
  79. ./ 1. Use MyCustomView() in Composable function @Composable fun ContentExample()

    { Column(Modifier.fillMaxSize()) { Text("This is MyCustomView") MyCustomView() } }
  80. 󰱢 Compose in RecyclerView

  81. • It’s not always feasible to migrate to LazyColumn. •

    Example: New view type introduced in product which is going to be displayed in existing list powered by RecyclerView. Firefox using composables in ViewHolder: github.com/mozilla-mobile/fenix/ Composable <> RecyclerView Source: developer.android.com/
  82. ./ 1. Create a ViewHolder abstract class ComposeViewHolder( val composeView:

    ComposeView, viewLifecycleOwner: LifecycleOwner ) : RecyclerView.ViewHolder(composeView) { @Composable abstract fun Content() init { composeView.setContent { AppTheme() { Content() } } } } Composable <> RecyclerView
  83. ./ 2. Define abstract Composable function and render it in

    init{} block. abstract class ComposeViewHolder( val composeView: ComposeView, viewLifecycleOwner: LifecycleOwner ) : RecyclerView.ViewHolder(composeView) { @Composable abstract fun Content() init { composeView.setContent { AppTheme() { Content() } } } } Composable <> RecyclerView
  84. ./ 3. Extend ComposeViewHolder and write UI in Compose class

    FeatureItemViewHolder( composeView: ComposeView, viewLifecycleOwner: LifecycleOwner, ) : ComposeViewHolder(composeView, viewLifecycleOwner) { @Composable override fun Content() { FeatureComposable(--.) } } Composable <> RecyclerView
  85. 🏗 Architecture in Jetpack Compose

  86. Unidirectional Data Flow (UDF) State Events UI User interactions trigger

    events Produce state Consume state
  87. Modeling state of a screen @Immutable data class ContactSearchUiState( val

    isLoading: Boolean, val contacts: List<Contact>, val searchQuery: String, ... )
  88. ViewModel - State holder class ContactSearchViewModel(...): ViewModel() { val state:

    StateFlow<ContactSearchUiState> = ... ... }
  89. UI state of a Screen @Composable fun ContactSearchScreen(viewModel: ContactSearchViewModel) {

    val state by viewModel.state.collectAsStateWithLifecycle() ContactSearchContent( contacts = state.contacts, onSearch = { query .> viewModel.search(query) }, ... ) }
  90. UI state of a Screen @Composable fun ContactSearchScreen(viewModel: ContactSearchViewModel) {

    val state by viewModel.state.collectAsStateWithLifecycle() ContactSearchContent( contacts = state.contacts, onSearch = { query .> viewModel.search(query) }, ... ) } Provide state
  91. UI state of a Screen @Composable fun ContactSearchScreen(viewModel: ContactSearchViewModel) {

    val state by viewModel.state.collectAsStateWithLifecycle() ContactSearchContent( contacts = state.contacts, onSearch = { query -> viewModel.search(query) }, ... ) } Handle Event
  92. 📈 Performance with Jetpack Compose

  93. Performance Tips ❌ Don’ts ❌ Calculations in Composable function. @Composable

    fun ContactsScreen(contacts: List<Contact>) { val sortedContacts = contacts.sortedBy { it.name } ContactList(sortedContacts, ...) }
  94. Performance Tips ❌ Don’ts ❌ Unstable types in Composable data

    class LoginState( var isLoading: Boolean, var userId: String ) @Composable fun LoginScreen(state: LoginState) { ... }
  95. Performance Tips ❌ Don’ts ❌ Backward writes @Composable fun Sample()

    { var count by remember { mutableStateOf(0) } Button(onClick = { count.+ }) { Text("+") } Text("$count") count-+ ./ Backwards write }
  96. Performance Tips ☑Do’s ✅ Use remember{} to minimize expensive calculations.

    @Composale fun ContactScreen(contacts: List<Contact>) { val sortedContacts = remember { contacts.sortedBy { it.name } } ... }
  97. Performance Tips ☑Do’s ✅ Use Stable/Immutable types in composable functions

    for smart recompositions. @Immutable data class LoginState( val isLoading: Boolean, val userId: String? ) fun LoginScreen(state: LoginState) { ... }
  98. Performance Tips ☑Do’s ✅ Defer reads as long as possible.

    @Composable fun Example(scrollOffset: Int) { ... Column( modifier = Modifier .offset(y = scrollOffset) ) { ./ ... } }
  99. Performance Tips ☑Do’s ✅ Defer reads as long as possible.

    @Composable fun Example(scrollProvider: () -> Int) { ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { ./ ... } }
  100. Measure performance with Android Studio profiler Source: medium.com/androiddevelopers/

  101. Measure UI Janks with Android Studio profiler Source: medium.com/androiddevelopers/

  102. Debug Recompositions with layout inspector Source: medium.com/androiddevelopers/

  103. Build app with R8 • Use R8 compiler to remove

    unnecessary code. • App size and performance can be improved with R8.
  104. That’s All 🫡

  105. 📚 Resources to learn Jetpack Compose

  106. Official Resources • developer.android.com/jetpack/compose • github.com/android/compose-samples • github.com/android/nowinandroid • medium.com/androiddevelopers

    • youtube.com/c/AndroidDevelopers/videos
  107. Unofficial Resources • jetpackcompose.app/ • compose.academy/ • github.com/Gurupreet/ComposeCookBook • github.com/PatilShreyas/NotyKT

  108. Thank you! Happy Composing 🚀 Shreyas Patil Google Dev Expert

    - Android Android @ Paytm shreyaspatil.dev