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

    View Slide

  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

    View Slide

  3. What is
    Jetpack Compose?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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?

    View Slide

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

    View Slide

  11. Why
    Jetpack Compose?

    View Slide

  12. Why
    Jetpack
    Compose?

    View Slide

  13. ● Single language - Kotlin
    Why
    Jetpack
    Compose?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. View vs Jetpack Compose
    -/ MainActivity.kt
    class MainActivity: Activity() {
    override fun onCreate(...) {
    setContentView(R.layout.activity_main)
    }
    }
    -/ activity_main.xml

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

    View Slide

  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?

    View Slide

  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?

    View Slide

  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?

    View Slide

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

    View Slide

  22. How to adopt
    Jetpack Compose?

    View Slide

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

    View Slide

  24. Migrating from View mindset
    and
    Thinking in
    Jetpack Compose
    🤔

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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.

    View Slide

  32. 📐
    Discuss and Plan

    View Slide

  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.

    View Slide

  34. 🎢
    Kickstarting adoption of
    Jetpack Compose

    View Slide

  35. 󰱢
    Migrating existing screen
    in Jetpack Compose

    View Slide

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

    View Slide

  37. ● Top App bar
    Components required for this screen

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  47. 󰱢
    Using Compose in Fragment

    View Slide

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

    View Slide

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

    View Slide

  50. 󰱢
    Theme interoperability
    XML < > Compose

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  54. // Theme defined in your app’s XML
    <br/><!-- Material 2 color attributes --><br/><item name="colorPrimary">@color/purple_500</item><br/><item name="colorSecondary">@color/green_200</item><br/><!-- Material 2 type attributes--><br/><item name="textAppearanceBody1">@style/TextAppearance.MyApp.Body1</item><br/><item name="textAppearanceBody2">@style/TextAppearance.MyApp.Body2</item><br/>
    Material Theme Compose Adapter

    View Slide

  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

    View Slide

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

    View Slide

  57. 󰱢

    Compose in Existing UI

    View Slide

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

    View Slide

  59. ./ 1. Place ComposeView in XML layout

    android:id="@+id/header_search_view"
    ... .>
    ...
    ./LinearLayout>
    Using Compose in Existing UI

    View Slide

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

    View Slide

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

    View Slide

  62. 󰱢

    Composable as Custom View

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  68. ./ 4. Place custom View in XML hierarchy

    ...
    android:id="@+id/chat_bottom_content"
    ... .>
    ./LinearLayout>
    Component as Custom View

    View Slide

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

    View Slide

  70. 󰱢

    Custom View as Composable

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  80. 󰱢
    Compose in RecyclerView

    View Slide

  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/

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  85. 🏗
    Architecture in
    Jetpack Compose

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  92. 📈
    Performance with
    Jetpack Compose

    View Slide

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

    View Slide

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

    View Slide

  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
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  104. That’s All 🫡

    View Slide

  105. 📚
    Resources to learn
    Jetpack Compose

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide