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

Adopting Jetpack Compose in your Android app - ...

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. • Modern UI development toolkit • Fully built with Kotlin

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

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

    • Simplifies and accelerates UI development • Less code • Intuitive Kotlin APIs What is Jetpack Compose?
  6. • 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?
  7. • Single language - Kotlin • UI and operations in

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

    same place • Declarative UI. Reactive pattern. • Loosely coupled code of UI Why Jetpack Compose?
  9. 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
  10. • 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?
  11. • 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?
  12. • 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?
  13. 1. Migrate mindset and thinking first 2. Discuss and plan

    3. Finally, kickstart adoption and migrate 🚀 Steps for adoption
  14. 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
  15. 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
  16. 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
  17. @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
  18. @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
  19. @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
  20. @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.
  21. 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.
  22. • Extract components into unit Composables. • Content from components.

    • Screen from content. Migrating existing Screen in Compose
  23. • Top App bar • Search Field • Split Button

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

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

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

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

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

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

    viewModel.state.collectAsState() ContactSearchContent(--.) } @Composable fun ContactSearchContent(...) {...} Assembling contents in Screen
  30. 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
  31. class NewFeatureFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater,

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

    container: ViewGroup?, savedInstanceState: Bundle? ): View = ComposeView(requireContext()).apply { setContent { NewFeatureScreen() } } } Using Composable in Fragment
  33. @Composable fun App() { MaterialTheme(...) { AppContent() } } Material

    Theme in Compose But what if your app already have Material Theme defined in styles.xml? 🤨
  34. // 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
  35. // 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
  36. // 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
  37. // 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? 🤨
  38. ./ 1. Place ComposeView in XML layout <LinearLayout ...> <androidx.compose.ui.platform.ComposeView

    android:id="@+id/header_search_view" ... .> ... ./LinearLayout> Using Compose in Existing UI
  39. ./ 3. Compose your UI with ComposeView val headerSearchView =

    findViewById(R.id.header_search_view) headerSearchView.setContent { PaytmHeaderSearch(...) } Using Compose in Existing UI
  40. ./ 1. Design a component in Jetpack Compose @Composable fun

    ChatBottomContent(...) { } Component as Custom View
  41. ./ 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
  42. ./ 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
  43. ./ 4. Place custom View in XML hierarchy <LinearLayout ...>

    ... <com.paytm.chat.ui.ChatBottomContentView android:id="@+id/chat_bottom_content" ... .> ./LinearLayout> Component as Custom View
  44. ./ 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
  45. @Composable fun <T : View> AndroidView( factory: (Context) .> T,

    modifier: Modifier = Modifier, update: (T) .> Unit = NoOpUpdate ): Unit Custom View as Composable
  46. 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 } ) }
  47. 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 } ) }
  48. 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 } ) }
  49. 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 } ) }
  50. 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 } ) }
  51. 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 } ) }
  52. 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 } ) }
  53. ./ 1. Use MyCustomView() in Composable function @Composable fun ContentExample()

    { Column(Modifier.fillMaxSize()) { Text("This is MyCustomView") MyCustomView() } }
  54. • 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/
  55. ./ 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
  56. ./ 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
  57. ./ 3. Extend ComposeViewHolder and write UI in Compose class

    FeatureItemViewHolder( composeView: ComposeView, viewLifecycleOwner: LifecycleOwner, ) : ComposeViewHolder(composeView, viewLifecycleOwner) { @Composable override fun Content() { FeatureComposable(--.) } } Composable <> RecyclerView
  58. Modeling state of a screen @Immutable data class ContactSearchUiState( val

    isLoading: Boolean, val contacts: List<Contact>, val searchQuery: String, ... )
  59. 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) }, ... ) }
  60. 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
  61. 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
  62. Performance Tips ❌ Don’ts ❌ Calculations in Composable function. @Composable

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

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

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

    @Composale fun ContactScreen(contacts: List<Contact>) { val sortedContacts = remember { contacts.sortedBy { it.name } } ... }
  66. 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) { ... }
  67. Performance Tips ☑Do’s ✅ Defer reads as long as possible.

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

    @Composable fun Example(scrollProvider: () -> Int) { ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { ./ ... } }
  69. Build app with R8 • Use R8 compiler to remove

    unnecessary code. • App size and performance can be improved with R8.
  70. Thank you! Happy Composing 🚀 Shreyas Patil Google Dev Expert

    - Android Android @ Paytm shreyaspatil.dev