Slide 1

Slide 1 text

Adopting Jetpack Compose in your Android app Shreyas Patil Google Dev Expert - Android Android @ Paytm shreyaspatil.dev

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

What is Jetpack Compose?

Slide 4

Slide 4 text

● Modern UI development toolkit What is Jetpack Compose?

Slide 5

Slide 5 text

● Modern UI development toolkit ● Fully built with Kotlin What is Jetpack Compose?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

● 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?

Slide 10

Slide 10 text

What is Jetpack Compose? Google Jetpack Compose v1.0.0 (July 2021)

Slide 11

Slide 11 text

Why Jetpack Compose?

Slide 12

Slide 12 text

Why Jetpack Compose?

Slide 13

Slide 13 text

● Single language - Kotlin Why Jetpack Compose?

Slide 14

Slide 14 text

● Single language - Kotlin ● UI and operations in same place Why Jetpack Compose?

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

● Single language - Kotlin ● UI and operations in same place ● Declarative UI. Reactive pattern. ● Loosely coupled code of UI Why Jetpack Compose?

Slide 17

Slide 17 text

View vs Jetpack Compose -/ MainActivity.kt class MainActivity: Activity() { override fun onCreate(...) { setContentView(R.layout.activity_main) } } -/ activity_main.xml ./LinearLayout> -/ MainActivity.kt class MainActivity: Activity() { override fun onCreate(...) { setContent { Greeting() } } } @Composable fun Greeting() { Text("Hello World!") } With View UI With Jetpack Compose UI

Slide 18

Slide 18 text

● 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?

Slide 19

Slide 19 text

● 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?

Slide 20

Slide 20 text

● 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?

Slide 21

Slide 21 text

Who is using Jetpack compose? See: developer.android.com/jetpack/compose/adopt

Slide 22

Slide 22 text

How to adopt Jetpack Compose?

Slide 23

Slide 23 text

1. Migrate mindset and thinking first 2. Discuss and plan 3. Finally, kickstart adoption and migrate 🚀 Steps for adoption

Slide 24

Slide 24 text

Migrating from View mindset and Thinking in Jetpack Compose 🤔

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

@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

Slide 29

Slide 29 text

@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

Slide 30

Slide 30 text

@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

Slide 31

Slide 31 text

@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.

Slide 32

Slide 32 text

📐 Discuss and Plan

Slide 33

Slide 33 text

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.

Slide 34

Slide 34 text

🎢 Kickstarting adoption of Jetpack Compose

Slide 35

Slide 35 text

󰱢 Migrating existing screen in Jetpack Compose

Slide 36

Slide 36 text

● Extract components into unit Composables. ● Content from components. ● Screen from content. Migrating existing Screen in Compose

Slide 37

Slide 37 text

● Top App bar Components required for this screen

Slide 38

Slide 38 text

● Top App bar ● Search Field Components required for this screen

Slide 39

Slide 39 text

● Top App bar ● Search Field ● Split Button Components required for this screen

Slide 40

Slide 40 text

● Top App bar ● Search Field ● Split Button ● List header Components required for this screen

Slide 41

Slide 41 text

● Top App bar ● Search Field ● Split Button ● List header ● List initial Components required for this screen

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

● Top App bar ● Search Field ● Split Button ● List header ● List initial ● Contact Item ● Item Scroller Components required for this screen

Slide 44

Slide 44 text

● Column ○ Column ■ Top App bar ■ Search Field ■ Split Button ■ List header ○ Box ■ LazyColumn(Contact Items) ■ Item Scroller Contents of this screen

Slide 45

Slide 45 text

@Composable fun ContactSearchScreen( viewModel: ContactSearchViewModel ) { val state by viewModel.state.collectAsState() ContactSearchContent(--.) } @Composable fun ContactSearchContent(...) {...} Assembling contents in Screen

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

󰱢 Using Compose in Fragment

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

󰱢 Theme interoperability XML < > Compose

Slide 51

Slide 51 text

@Composable fun App() { MaterialTheme(...) { AppContent() } } Material Theme in Compose But what if your app already have Material Theme defined in styles.xml? 🤨

Slide 52

Slide 52 text

Material Theme Compose Adapter Source: https://github.com/material-components/material-components-android-compose-theme-adapter

Slide 53

Slide 53 text

// 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

Slide 54

Slide 54 text

// Theme defined in your app’s XML <!-- 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> Material Theme Compose Adapter

Slide 55

Slide 55 text

// 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

Slide 56

Slide 56 text

// 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? 🤨

Slide 57

Slide 57 text

󰱢 Compose in Existing UI

Slide 58

Slide 58 text

Example: Search Field powered by Jetpack Compose in Paytm home screen. Using Compose in Existing UI

Slide 59

Slide 59 text

./ 1. Place ComposeView in XML layout ... ./LinearLayout> Using Compose in Existing UI

Slide 60

Slide 60 text

./ 2. Retrieve ComposeView in Activity val headerSearchView = findViewById(R.id.header_search_view) Using Compose in Existing UI

Slide 61

Slide 61 text

./ 3. Compose your UI with ComposeView val headerSearchView = findViewById(R.id.header_search_view) headerSearchView.setContent { PaytmHeaderSearch(...) } Using Compose in Existing UI

Slide 62

Slide 62 text

󰱢 Composable as Custom View

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

./ 1. Design a component in Jetpack Compose @Composable fun ChatBottomContent(...) { } Component as Custom View

Slide 66

Slide 66 text

./ 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

Slide 67

Slide 67 text

./ 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("") ... @Composable override fun Content() { PaytmTheme { ChatBottomContent(message, ...) } } } Component as Custom View

Slide 68

Slide 68 text

./ 4. Place custom View in XML hierarchy ... ./LinearLayout> Component as Custom View

Slide 69

Slide 69 text

./ 5. Access View in Activity/Fragment val bottomContentView = findViewById( R.id.chat_bottom_content ) ./ Access properties bottomContentView.message = "Hi there!" ./ Access events bottomContentView.onSendClick { sendMessage(bottomContentView.message) } Component as Custom View

Slide 70

Slide 70 text

󰱢 Custom View as Composable

Slide 71

Slide 71 text

@Composable fun AndroidView( factory: (Context) .> T, modifier: Modifier = Modifier, update: (T) .> Unit = NoOpUpdate ): Unit Custom View as Composable

Slide 72

Slide 72 text

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 } ) }

Slide 73

Slide 73 text

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 } ) }

Slide 74

Slide 74 text

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 } ) }

Slide 75

Slide 75 text

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 } ) }

Slide 76

Slide 76 text

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 } ) }

Slide 77

Slide 77 text

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 } ) }

Slide 78

Slide 78 text

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 } ) }

Slide 79

Slide 79 text

./ 1. Use MyCustomView() in Composable function @Composable fun ContentExample() { Column(Modifier.fillMaxSize()) { Text("This is MyCustomView") MyCustomView() } }

Slide 80

Slide 80 text

󰱢 Compose in RecyclerView

Slide 81

Slide 81 text

● 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/

Slide 82

Slide 82 text

./ 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

Slide 83

Slide 83 text

./ 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

Slide 84

Slide 84 text

./ 3. Extend ComposeViewHolder and write UI in Compose class FeatureItemViewHolder( composeView: ComposeView, viewLifecycleOwner: LifecycleOwner, ) : ComposeViewHolder(composeView, viewLifecycleOwner) { @Composable override fun Content() { FeatureComposable(--.) } } Composable <> RecyclerView

Slide 85

Slide 85 text

🏗 Architecture in Jetpack Compose

Slide 86

Slide 86 text

Unidirectional Data Flow (UDF) State Events UI User interactions trigger events Produce state Consume state

Slide 87

Slide 87 text

Modeling state of a screen @Immutable data class ContactSearchUiState( val isLoading: Boolean, val contacts: List, val searchQuery: String, ... )

Slide 88

Slide 88 text

ViewModel - State holder class ContactSearchViewModel(...): ViewModel() { val state: StateFlow = ... ... }

Slide 89

Slide 89 text

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) }, ... ) }

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

📈 Performance with Jetpack Compose

Slide 93

Slide 93 text

Performance Tips ❌ Don’ts ❌ Calculations in Composable function. @Composable fun ContactsScreen(contacts: List) { val sortedContacts = contacts.sortedBy { it.name } ContactList(sortedContacts, ...) }

Slide 94

Slide 94 text

Performance Tips ❌ Don’ts ❌ Unstable types in Composable data class LoginState( var isLoading: Boolean, var userId: String ) @Composable fun LoginScreen(state: LoginState) { ... }

Slide 95

Slide 95 text

Performance Tips ❌ Don’ts ❌ Backward writes @Composable fun Sample() { var count by remember { mutableStateOf(0) } Button(onClick = { count.+ }) { Text("+") } Text("$count") count-+ ./ Backwards write }

Slide 96

Slide 96 text

Performance Tips ☑Do’s ✅ Use remember{} to minimize expensive calculations. @Composale fun ContactScreen(contacts: List) { val sortedContacts = remember { contacts.sortedBy { it.name } } ... }

Slide 97

Slide 97 text

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) { ... }

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

Performance Tips ☑Do’s ✅ Defer reads as long as possible. @Composable fun Example(scrollProvider: () -> Int) { ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { ./ ... } }

Slide 100

Slide 100 text

Measure performance with Android Studio profiler Source: medium.com/androiddevelopers/

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

Build app with R8 ● Use R8 compiler to remove unnecessary code. ● App size and performance can be improved with R8.

Slide 104

Slide 104 text

That’s All 🫡

Slide 105

Slide 105 text

📚 Resources to learn Jetpack Compose

Slide 106

Slide 106 text

Official Resources ● developer.android.com/jetpack/compose ● github.com/android/compose-samples ● github.com/android/nowinandroid ● medium.com/androiddevelopers ● youtube.com/c/AndroidDevelopers/videos

Slide 107

Slide 107 text

Unofficial Resources ● jetpackcompose.app/ ● compose.academy/ ● github.com/Gurupreet/ComposeCookBook ● github.com/PatilShreyas/NotyKT

Slide 108

Slide 108 text

Thank you! Happy Composing 🚀 Shreyas Patil Google Dev Expert - Android Android @ Paytm shreyaspatil.dev