Let's Talk Composing UI :: DevFest New Delhi

Let's Talk Composing UI :: DevFest New Delhi

Visit: https://rivu.dev for more info.
While Developing Apps, following a Reactive Architecture (for example MVI, Mobius, Redux and even MVVM) & Single Source of Truth can get you some big wins including but not limited to, Loose Coupling & Separation of Concerns, Code Testability and easy debugging, unidirectional data, etc. However, unlike Web, where Reactive Architectures are the norm, in Android, we need to opt for Reactive Architecture considering few tradeoffs (time to market, learning curve, etc.), as it’s not natural in Android.

What do I mean by saying Reactive architecture is not natural in Android? Just like web frontends, Android apps revolve around the UI, everything we do in our Android apps has some direct or indirect relation with the UI, and the Android UI framework is imperative itself.
While the Android Platform team kept adding more and more types of Views (such as Constraint Layout, RecyclerView or more recent Motion Layout), they didn’t change the nature of UI framework itself since the beginning of android development. This was majorly due to language and tooling limitations (limited by the technologies of that time).

Google announced official support for Kotlin 2 years back, and now major numbers of professional Android Developers worldwide already adopted Kotlin, which made it easier for Google to go Kotlin first in this IO19. Kotlin comes with many perks, some of them are Functional Programming support, compiler plugin capabilities and most importantly huge support on building DSLs at ease.

What relation does Koltin have with Reactive applications? In this IO19, Google announced Jetpack Compose (https://developer.android.com/jetpack/compose/), a new (still-in-development) next generation, Kotlin based, reactive cum declarative UI toolkit, backed by principals like Single Source of Truth, Unidirectional Data Flow, Functional Programming (especially function composition and effects).

This new UI toolkit would require a radical shift in our thought process about app architectures UI programming. In this talk, we would see how we can create and interact with UI with Jetpack Compose and how different it is from the present Android framework. We will also look into examples of some code patterns and ideas of from a few famous platforms such as Vue.js, React, Flutters, etc., and how these patterns and are adopted in Jetpack Compose.

36c29634c5d55eae66224c24ba2b933c?s=128

Rivu Chakraborty

September 29, 2019
Tweet

Transcript

  1. Hello Delhi!! Slide Design by instagram.com/mohijeet/ Rivu Chakraborty

  2. Let’s Talk Composing UI Rivu Chakraborty https://rivu.dev New Delhi

  3. Rivu Chakraborty About Me • https://rivu.dev • Sr Software Engineer

    (Android) - BYJU’S • Instructor - Caster.io • Google Certified Associate Android Developer • Speaks on Kotlin / Android • Author - Reactive Programming in Kotlin • Author - Functional Kotlin • Author - Hands-On Data Structures and Algorithms with Kotlin
  4. None
  5. @intelligibabble http://intelligiblebabble.com/ Big Shout Out Leland Richardson

  6. Before We begin • Android Developers @rivuchakraborty https://rivu.dev

  7. • Android Developers • Used Kotlin @rivuchakraborty https://rivu.dev Before We

    begin
  8. • Android Developers • Used Kotlin • Loves writing XML?

    @rivuchakraborty https://rivu.dev Before We begin
  9. • Android Developers • Used Kotlin • Loves writing XML?

    • Used / Loves Declarative UI @rivuchakraborty https://rivu.dev Before We begin
  10. • Android Developers • Used Kotlin • Loves writing XML?

    • Used / Loves Declarative UI • Functional / Reactive Programming / Using Redux @rivuchakraborty https://rivu.dev Before We begin
  11. This Talk Covers • Why should we care @rivuchakraborty https://rivu.dev

  12. @rivuchakraborty https://rivu.dev This Talk Covers • Why should we care

    • How to Use Jetpack Compose
  13. @rivuchakraborty https://rivu.dev This Talk Covers • Why should we care

    • How to Use Jetpack Compose • How Compose Works
  14. This Talk Covers @rivuchakraborty https://rivu.dev • Why should we care

    • How to Use Jetpack Compose • How Compose Works • How to manage States with Compose
  15. @rivuchakraborty https://rivu.dev Why Should We Care

  16. Why Should We Care @rivuchakraborty https://rivu.dev

  17. @rivuchakraborty https://rivu.dev Why Should We Care

  18. @rivuchakraborty https://rivu.dev Why Should We Care

  19. @rivuchakraborty https://rivu.dev Why Should We Care

  20. @rivuchakraborty https://rivu.dev Why Should We Care

  21. Why Should We Care @rivuchakraborty https://rivu.dev

  22. Why Should We Care • Functional • Declarative • Reactive

    • UI @rivuchakraborty https://rivu.dev
  23. Why Should We Care @rivuchakraborty https://rivu.dev Card(color = cardInternalColor) {

    Padding(padding = 12.dp) { Column { Row { ... } Row { ... } } } } Declarative UI
  24. Why Should We Care @rivuchakraborty https://rivu.dev Declarative UI
 Circular Image

    imageFromResource( res = context.resources, R.drawable.my_drawable, shape = CircleBorder() )
  25. Why Should We Care • UI • Declarative • Functional

    • Reactive Functional Declarative UI is on rise to become The Way of doing UI @rivuchakraborty https://rivu.dev
  26. Why Should We Care Benefits • Separation of Concerns @rivuchakraborty

    https://rivu.dev
  27. Why Should We Care Benefits • Separation of Concerns •

    Declarative + Functional / Reactive @rivuchakraborty https://rivu.dev
  28. Why Should We Care Benefits • Separation of Concerns •

    Declarative + Functional / Reactive • DSL @rivuchakraborty https://rivu.dev
  29. Why Should We Care Benefits • Separation of Concerns •

    Declarative + Functional / Reactive • DSL • Developer Friendly @rivuchakraborty https://rivu.dev
  30. Why Should We Care @rivuchakraborty https://rivu.dev

  31. Why Should We Care Benefits • Flattened UI @rivuchakraborty https://rivu.dev

  32. Why Should We Care Benefits • Flattened UI • Less

    Compile Time Overhead @rivuchakraborty https://rivu.dev
  33. Why Should We Care Benefits • Flattened UI • Less

    Compile Time Overhead • Better Performance, Less Memory Wastage @rivuchakraborty https://rivu.dev
  34. Why Should We Care Benefits • Flattened UI • Less

    Compile Time Overhead • Better Performance, Less Memory Wastage • Easier State Management @rivuchakraborty https://rivu.dev
  35. How to use Jetpack Compose

  36. How to use Jetpack Compose Everything is `fun` And Lambda

  37. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun CustomText(text:

    String) { Text(text = text, textAlign = TextAlign.Center) }
  38. How to use Jetpack Compose @Composable @rivuchakraborty https://rivu.dev

  39. How to use Jetpack Compose @rivuchakraborty https://rivu.dev

  40. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun CustomText(text:

    String) { Text(text = text, textAlign = TextAlign.Center) }
  41. How to use Jetpack Compose @rivuchakraborty https://rivu.dev Let’s build a

    simple App
  42. How to use Jetpack Compose @rivuchakraborty https://rivu.dev

  43. How to use Jetpack Compose @rivuchakraborty https://rivu.dev setContent { CraneWrapper

    { CustomTheme { Counter() } } }
  44. CraneWrapper: Needed to buld the Foundation Might be renamed/removed later

    or get invoked by the framework itself inside `setContent()` How to use Jetpack Compose @rivuchakraborty https://rivu.dev
  45. MaterialTheme / CustomTheme: Every Root Layout (Anything inside CraneWrapper), should

    have a Theme How to use Jetpack Compose @rivuchakraborty https://rivu.dev setContent { CraneWrapper { CustomTheme { Counter() } } }
  46. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun CustomTheme(children:

    @Composable() () -> Unit) { MaterialTheme(colors=myColorList, typography = myTextStyles) { CurrentTextStyleProvider(defaultTextStyle) { children() } } }
  47. How to use Jetpack Compose @rivuchakraborty https://rivu.dev setContent { CraneWrapper

    { CustomTheme { Counter() } } }
  48. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { Text( style = +themeTextStyle { h4 }, text = "Count 0" ) Button( text = "Increase", shape = CircleBorder(), onClick = { //Counter Increment Logic } ) }
  49. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { Text( style = +themeTextStyle { h4 }, text = "Count 0" ) Button( text = “Increase”, shape = CircleBorder(), onClick = { //Counter Increment Logic } ) }
  50. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { Text( style = +themeTextStyle { h4 }, text = "Count 0" ) Button( text = "Increase", shape = CircleBorder(), onClick = { //Counter Increment Logic } ) }
  51. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { Text( style = +themeTextStyle { h4 }, text = "Count 0" ) Button( text = "Increase", shape = CircleBorder(), onClick = { //Counter Increment Logic } ) }
  52. How to use Jetpack Compose @rivuchakraborty https://rivu.dev

  53. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { Column { Row { Padding(padding = 5.dp) { Text( style = +themeTextStyle { h4 }, text = "Count 0" ) } } Row { Padding(padding = 5.dp) { Button( text = "Increase", onClick = { //Counter Increment Logic } ) } } } }
  54. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { Column { Row { Padding(padding = 5.dp) { Text( style = +themeTextStyle { h4 }, text = "Count 0" ) } }
  55. How to use Jetpack Compose @rivuchakraborty https://rivu.dev } } Row

    { Padding(padding = 5.dp) { Button( text = "Increase", onClick = { //Counter Increment Logic } ) } } } }
  56. @rivuchakraborty https://rivu.dev How to use Jetpack Compose

  57. Let’s write the logic for Counter How to use Jetpack

    Compose @rivuchakraborty https://rivu.dev
  58. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { val countState = +state { 0 } Column { Row { Padding(padding = 5.dp) { Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) } } Row { Padding(padding = 5.dp) { Button( text = "Increase", onClick = { countState.value++ } ) } } } }
  59. How to use Jetpack Compose @rivuchakraborty https://rivu.dev @Composable fun Counter()

    { val countState = +state { 0 } Column { Row { Padding(padding = 5.dp) { Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) } } Row {
  60. How to use Jetpack Compose @rivuchakraborty https://rivu.dev } Row {

    Padding(padding = 5.dp) { Button( text = "Increase", onClick = { countState.value++ } ) } } } }
  61. How to use Jetpack Compose @rivuchakraborty https://rivu.dev

  62. How Compose Works

  63. How Compose Works Gap Buffer @rivuchakraborty https://rivu.dev

  64. Item 1 Item 2 Item 3 Empty Empty Empty Empty

    How Compose Works Gap Buffer @rivuchakraborty https://rivu.dev
  65. How Compose Works Gap Buffer Slot Table @rivuchakraborty https://rivu.dev

  66. How Compose Works Slot Table Item 1 Item 2 Item

    3 Empty Empty Empty Empty Cu rr en t In de x @rivuchakraborty https://rivu.dev
  67. Item 1 Item 2 Item 3 Item 4 Empty Empty

    Empty Cu rr en t In de x @rivuchakraborty https://rivu.dev How Compose Works Slot Table
  68. @rivuchakraborty https://rivu.dev How Compose Works @Composable fun Counter2() { val

    countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  69. Group Empty Empty Empty Empty Empty Empty How Compose Works

    @rivuchakraborty https://rivu.dev @Composable fun Counter2() { val countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  70. Group State(0) Empty Empty Empty Empty Empty How Compose Works

    @rivuchakraborty https://rivu.dev @Composable fun Counter2() { val countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  71. Group State(0) Group Empty Empty Empty Empty How Compose Works

    @rivuchakraborty https://rivu.dev @Composable fun Counter2() { val countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  72. Group State(0) Group “Count 0” Empty Empty Empty How Compose

    Works @rivuchakraborty https://rivu.dev @Composable fun Counter2() { val countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  73. Group State(0) Group “Count 0” Group Empty Empty How Compose

    Works @rivuchakraborty https://rivu.dev @Composable fun Counter2() { val countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  74. Group State(0) Group “Count 0” Group {...} Empty How Compose

    Works @rivuchakraborty https://rivu.dev @Composable fun Counter2() { val countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  75. Group State(1) Group “Count 1” Group {...} Empty Recompose How

    Compose Works @rivuchakraborty https://rivu.dev @Composable fun Counter2() { val countState = +state { 0 } Text( style = +themeTextStyle { h4 }, text = "Count ${countState.value}" ) Button( text = "Increase", onClick = { countState.value++ } ) }
  76. View Tree with XML in Present Day View Group 1

    View Group 2 View Group 3 View Group 4 Image View Button Text View Toolbar @rivuchakraborty https://rivu.dev How Compose Works
  77. Slot Table Group State(1) Group “Count 1” “Group” {...} Empty

    Cu rr en t In de x How Compose Works @rivuchakraborty https://rivu.dev
  78. How to Manage States with Compose How should we align

    our thinking to work with Functional Declarative UIs
  79. How to Manage States with Compose State View Action Reactive

    - Redux, Mobx, MVI, Mobius, MVPI, MVVMI ... @rivuchakraborty https://rivu.dev
  80. State How to Manage States with Compose @rivuchakraborty https://rivu.dev sealed

    class ViewState: BaseState { object Loading: ViewState() data class Success(val result: Data): ViewState() data class Failure(val error: Throwable): ViewState() }
  81. View How to Manage States with Compose /////MVIActivity.kt override fun

    intents(): Observable<HomeIntents> { ... } override fun bind() { viewModel.processIntents(intents()) viewModel.states().observe(this, Observer { ... } ) }
  82. ViewModel How to Manage States with Compose private val intentsSubject:

    PublishSubject<I> = PublishSubject.create() val statesObservable: Flowable<S> by lazy { intentsSubject .map(...) ... } override fun processIntents(intents: Observable<I>) { intents.subscribe(intentsSubject) }
  83. ViewModel How to Manage States with Compose ... } override

    fun processIntents(intents: Observable<I>) { intents.subscribe(intentsSubject) } override fun states(): LiveData<S> { return LiveDataReactiveStreams.fromPublisher<S>(statesObservable) }
  84. The Problem How to Manage States with Compose etSearch.addTextChangeListener(object: TextWatcher

    { override fun beforeTextChanged(c:CharSequence, start: Int, count: Int, after: Int) { ... } override fun onTextChanged(c:CharSequence, start: Int, count: Int, after: Int) { ... } override fun afterTextChanged(s: Editable) { ... } }
  85. Spinner SpinnerAdapter OnItemSelectedListener How to Manage States with Compose The

    Problem 2 @rivuchakraborty https://rivu.dev
  86. Jetpack Compose How to Manage States with Compose The Solution

    @rivuchakraborty https://rivu.dev
  87. Let’s build a statefull App How to Manage States with

    Compose @rivuchakraborty https://rivu.dev
  88. How to Manage States with Compose @rivuchakraborty https://rivu.dev data class

    User( var name: String="", var email: String="" )
  89. How to Manage States with Compose @rivuchakraborty https://rivu.dev sealed class

    UserViewState { object Loading: UserViewState() data class Success( val user: User ): UserViewState() data class Failure( val errorDetails: String ): UserViewState() }
  90. How to Manage States with Compose @rivuchakraborty https://rivu.dev class UserViewModel:

    BaseViewModel<UserIntents, UserViewState, UserActions, UserResults> { override fun states(): LiveData<UserViewState> }
  91. How to Manage States with Compose @rivuchakraborty https://rivu.dev interface Presenter

    { fun getUserAsync(): Observable<UserViewState> }
  92. UI How to Manage States with Compose @rivuchakraborty https://rivu.dev

  93. Success How to Manage States with Compose @rivuchakraborty https://rivu.dev @Composable

    fun Body(user: User, onReloadClick: () -> Unit) { Padding(padding = 16.dp) { Column { Row { Text { Span(text = "Name: ",style = +themeTextStyle { body1 } ) Span(text = user.name, style = +themeTextStyle { h6 } ) } }
  94. Span(text = user.name, style = +themeTextStyle { h6 } )

    } } Row { Text { Span(text = "Email: ",style = +themeTextStyle { body1 } ) Span(text = user.email, style = +themeTextStyle { body1 } ) } } } } Success How to Manage States with Compose @rivuchakraborty https://rivu.dev
  95. Span(text = "Email: ",style = +themeTextStyle { body1 } )

    Span(text = user.email, style = +themeTextStyle { body1 } ) } } } } Align(Alignment.Center) { ReloadButton(onReloadClick) } } Success How to Manage States with Compose @rivuchakraborty https://rivu.dev
  96. Success How to Manage States with Compose @rivuchakraborty https://rivu.dev @Composable

    fun Body(user: User, onReloadClick: () -> Unit) { Padding(padding = 16.dp) { Column { Row { Text { Span(text = "Name: ",style = +themeTextStyle { body1 }) Span(text = user.name, style = +themeTextStyle { h6 }) } } Row { Text { Span(text = "Email: ",style = +themeTextStyle { body1 }) Span(text = user.email, style = +themeTextStyle { h6 }) } } } } Align(Alignment.Center) { ReloadButton(onReloadClick) } }
  97. Error How to Manage States with Compose @rivuchakraborty https://rivu.dev @Composable

    fun ErrorBody(onReloadClick: () -> Unit) { Align(Alignment.Center) { Column { Row { Text(text = "Load failed", style = +themeTextStyle { body1 } ) } Row { ReloadButton(onReloadClick) } } } }
  98. Reload Button How to Manage States with Compose @rivuchakraborty https://rivu.dev

    @Composable fun ReloadButton(onReloadClick: () -> Unit) { Button(onClick = onReloadClick, text = "Reload", color = +themeColor { lightGray }) }
  99. How to Manage States with Compose @rivuchakraborty https://rivu.dev sealed class

    UserViewState { object Loading: UserViewState() data class Success( val user: User ): UserViewState() data class Failure( val errorDetails: String ): UserViewState() }
  100. State change In the UI Level 2 ways How to

    Manage States with Compose @rivuchakraborty https://rivu.dev sealed class UserViewState { object Loading: UserViewState() data class Success( val user: User ): UserViewState() data class Failure( val errorDetails: String ): UserViewState() }
  101. Activity How to Manage States with Compose Approach 1 @rivuchakraborty

    https://rivu.dev class StateExampleActivity : Activity() { @Inject lateinit var presenter: Presenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CraneWrapper { CustomTheme { StateComposable(presenter) } } } } }
  102. Activity How to Manage States with Compose Approach 1 @rivuchakraborty

    https://rivu.dev class StateExampleActivity : Activity() { @Inject lateinit var presenter: Presenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CraneWrapper { CustomTheme {
  103. lateinit var presenter: Presenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent { CraneWrapper { CustomTheme { StateComposable(presenter) } } } } } Activity Approach 1 @rivuchakraborty https://rivu.dev
  104. How to Manage States with Compose Approach 1 Root Composable

    @rivuchakraborty https://rivu.dev @Composable fun StateComposable( presenter: Presenter, stateModel: UserViewState = UserViewState.Loading ) { val data = +state { stateModel } fun onReloadClick() { data.value = UserViewState.Loading }
  105. ) { val data = +state { stateModel } fun

    onReloadClick() { data.value = UserViewState.Loading } when (stateModel) { is UserViewState.Loading -> { +onCommit { presenter.getUserAsync() .subscribe { userViewState -> Approach 1 Root Composable @rivuchakraborty https://rivu.dev
  106. } when (stateModel) { is UserViewState.Loading -> { +onCommit {

    presenter.getUserAsync() .subscribe { userViewState -> data.value = userViewState } } Align(Alignment.Center) { Text(text = "Loading", style = +themeTextStyle { h2 }) Approach 1 Root Composable @rivuchakraborty https://rivu.dev
  107. return } is UserViewState.Failure -> ErrorBody(::onReloadClick) is UserViewState.Success -> {

    val user = stateModel.user Body(user, ::onReloadClick) } } } Approach 1 Root Composable @rivuchakraborty https://rivu.dev
  108. Model Class How to Manage States with Compose @rivuchakraborty https://rivu.dev

    Approach 2 @Model class ViewStateModel( var user: User? = null, var error: String = "", var isLoading: Boolean = false )
  109. Activity How to Manage States with Compose Approach 2 @rivuchakraborty

    https://rivu.dev class ModelClassExampleActivity : Activity() { var stateModel = ViewStateModel(isLoading = true) @Inject lateinit var presenter: Presenter val compositeDisposable = CompositeDisposable() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
  110. val compositeDisposable = CompositeDisposable() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    setContent { CraneWrapper { CustomTheme { ModelComposable(stateModel,::loadData) } } } loadData() } Activity Approach 2 @rivuchakraborty https://rivu.dev
  111. } private fun loadData() { stateModel.isLoading = true val disposable

    = presenter.getUserAsync() .subscribe { when (it) { is UserViewState.Loading -> stateModel.isLoading = true is UserViewState.Failure -> stateModel.error = it.errorDetails is UserViewState.Success -> stateModel.user = it.user } Approach 2 @rivuchakraborty https://rivu.dev Activity
  112. } compositeDisposable.add(disposable) } override fun onDestroy() { super.onDestroy() compositeDisposable.dispose() }

    } Approach 2 @rivuchakraborty https://rivu.dev Activity
  113. Composable How to Manage States with Compose @rivuchakraborty https://rivu.dev Approach

    2 @Composable fun ModelComposable(viewStateModel: ViewStateModel, onReloadClick: () -> Unit) { val user = viewStateModel.user if(viewStateModel.isLoading) { Align(Alignment.Center) { Text(text = "Loading", style = +themeTextStyle { h2 }) } } else if(viewStateModel.error.isNotBlank() || user == null) {
  114. if(viewStateModel.isLoading) { Align(Alignment.Center) { Text(text = "Loading", style = +themeTextStyle

    { h2 }) } } else if(viewStateModel.error.isNotBlank() || user == null) { ErrorBody(onReloadClick) } else { Body(user, onReloadClick) } } @rivuchakraborty https://rivu.dev
  115. Let’s Talk Composing UI
 Take Aways @rivuchakraborty https://rivu.dev ✓Try out

    Jetpack Compose ✓Play with +state, +model ✓Try @Model class ✓Try using your preferred Architecture Pattern with Jetpack Compose
  116. Let’s Talk Composing UI
 Resources ➢ https://developer.android.com/jetpack/compose ➢ http://bit.ly/composefirstprinciple ➢

    http://bit.ly/contentondeclarativeUI ➢ https://speakerdeck.com/lelandrichardson/react-meet-compose ➢ https://rivu.dev/writing-android-ui-code-in-jetpack-compose/ ➢ https://rivu.dev/jetpack-compose-managing-states/ ➢ https://fragmentedpodcast.com/episodes/172/ @rivuchakraborty https://rivu.dev
  117. .droidcon India https://in.droidcon.com/

  118. .droidcon India https://in.droidcon.com/ devfestnd

  119. धन्यवाद!! Thank You!! @rivuchakraborty https://rivu.dev Slide Design by instagram.com/mohijeet/