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

Android UIs at Scale: UI Architecture in the Compose world

Android UIs at Scale: UI Architecture in the Compose world

These are the slides from the 360AnDev 2021 version of the talk "Android UIs at Scale: UI Architecture in the Compose world".

Android UIs are experiencing a paradigm shift with the advent of Jetpack Compose, and it's necessary for us to prepare for it.

As we navigate these weird times for UI development in Android, we need to have a strategy in place to adopt Compose, but how can we embrace Compose, while maintaining our current UI in a good state? What does an architecture that plays nice with Views and Composables look like?

In this session I will describe the mechanisms that we use at Twitter to create UI components. I will explain how we manage their reusability and encapsulation, how our stateful UI components look like, how they work with Android Views and Composables and how the migration path between them.

575f7b9c8722a6b4fec3d47c69d473c9?s=128

Nacho L.

July 23, 2021
Tweet

Transcript

  1. UI Architecture in the Compose world Android UIs at Scale

    360|AnDev 2021
  2. Nacho Lopez He/Him Software Engineer Android Core UI @mrmans0n

  3. [CHIRP BIRD ICO N] UI Architecture

  4. A long time ago in a galaxy far, far away

  5. A long time ago in a galaxy far, far away

    … Well, actually… it was in 2017
  6. Laissez-faire architecture A long time ago in a galaxy far,

    far away …
  7. Laissez-faire architecture A long time ago in a galaxy far,

    far away …
  8. Laissez-faire architecture A long time ago in a galaxy far,

    far away …
  9. Laissez-faire architecture A long time ago in a galaxy far,

    far away …
  10. It needed to stop

  11. None
  12. None
  13. None
  14. None
  15. None
  16. None
  17. None
  18. None
  19. 19 interface ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel:

    ViewModel): Disposable } interface ViewDelegate<out V: View> { protected val rootView: V }
  20. 20 interface ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel:

    ViewModel): Disposable } interface ViewDelegate<out V: View> { protected val rootView: V } But it still was too unopinionated
  21. But it still was too unopinionated 3 classes

  22. But it still was too unopinionated 3 classes Weak contract

  23. But it still was too unopinionated 3 classes Weak contract

    RxJava++
  24. So how did we fix that?

  25. None
  26. None
  27. None
  28. ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable

    } interface ViewDelegate<out V: View> { protected val rootView: V } interface
  29. ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable

    } interface ViewDelegate<out V: View> { protected val rootView: V } interface
  30. ViewModel class MyViewModel : { internal fun textChanged(): Observable<String> =

    ... internal fun respondToButtonClick() { ... } fun updateText(text: String) { ... } }
  31. ViewModel class MyViewModel : { internal fun textChanged(): Observable<String> =

    ... } internal fun onButtonClicked(): Observable<View> = button.clicks() }
  32. ) : ViewDelegate<ConstraintLayout> { private val button = rootView.findViewById<Button>(R.id.button1) internal

    fun setText(text: String) { .
  33. ) : ViewDelegate<ConstraintLayout> { private val button = rootView.findViewById<Button>(R.id.button1) internal

    fun setText(text: String) { .
  34. Merge all the streams into one to create a stronger

    contract
  35. interface ViewModel<VS: ViewState, I: UserIntent> { fun stateObservable(): Observable<VS> fun

    processUserIntent(intent: I) } class WeaverViewBinder : Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable { val disposables = CompositeDisposable() disposables += viewModel.stateObservable() .subscribe { state >
  36. interface ViewModel<VS: ViewState, I: UserIntent> { fun stateObservable(): Observable<VS> fun

    processUserIntent(intent: I) } interface ViewDelegate<out V: View, VS: ViewState, I: UserIntent> { protected val rootView: V fun render(state: VS) fun userIntentObservable(): Observable<I> = Observable.never() } class WeaverViewBinder : Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable { val disposables = CompositeDisposable() disposables += viewModel.stateObservable() .subscribe { state >
  37. interface ViewDelegate<out V: View, VS: ViewState, I: UserIntent> { protected

    val rootView: V fun render(state: VS) fun userIntentObservable(): Observable<I> = Observable.never() } class WeaverViewBinder : Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable { val disposables = CompositeDisposable() disposables += viewModel.stateObservable() .subscribe { state -> viewDelegate.render(state) } disposables += viewDelegate.userIntentObservable() .subscribe { intent -> viewModel.processUserIntent(intent) } return disposables } }
  38. interface ViewDelegate<out V: View, VS: ViewState, I: UserIntent> { protected

    val rootView: V fun render(state: VS) fun userIntentObservable(): Observable<I> = Observable.never() } class WeaverViewBinder : Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable { val disposables = CompositeDisposable() disposables += viewModel.stateObservable() .subscribe { state >
  39. interface ViewDelegate<out V: View, VS: ViewState, I: UserIntent> { protected

    val rootView: V fun render(state: VS) fun userIntentObservable(): Observable<I> = Observable.never() } class WeaverViewBinder : Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable { val disposables = CompositeDisposable() disposables += viewModel.stateObservable() .subscribe { state >
  40. VM MV

  41. VM MVI

  42. What we use: Weaver [CHIRPBIRDI CO N] ’s MVI +

    Data binding UI architecture framework
  43. What is Weaver?

  44. 1. Data binding What is Weaver?

  45. 1. Data binding What is Weaver? <FrameLayout android:id="@+id/my_layout" ... >

    <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout>
  46. 1. Data binding What is Weaver? <FrameLayout android:id="@+id/my_layout" ... >

    <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout>
  47. 1. Data binding What is Weaver? <FrameLayout android:id="@+id/my_layout" ... >

    <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout>
  48. 1. Data binding What is Weaver? <FrameLayout android:id="@+id/my_layout" ... >

    <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout>
  49. 1. Data binding What is Weaver? <FrameLayout android:id="@+id/my_layout" ... >

    <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout> 2. MVI
  50. 2. MVI What is Weaver?

  51. 2. MVI What is Weaver? setState { ... }

  52. 2. MVI What is Weaver? setState { -> ... }

  53. 2. MVI What is Weaver? setState { ... ) useState

    { state .
  54. 2. MVI What is Weaver? setState { ... } onSuccess

    { value -> ... } onFailure { throwable -> ... } onSuccessOrFailure { result -> ... } onComplete { ... } } yieldEffect( .
  55. 2. MVI What is Weaver? setState { -> ... }

    yieldEffect( .
  56. What is Weaver? 3. Unidirectional Data Flow

  57. What is Weaver? 3. Unidirectional Data Flow

  58. What is Weaver? 3. Unidirectional Data Flow

  59. What is Weaver? 3. Unidirectional Data Flow

  60. What is Weaver? 3. 4. Stand comp Unidirectional Data Flow

  61. 4. Standalone components What is Weaver?

  62. What is Weaver? 4. Standalone components class MyFeatureActivity : InjectedFragmentActivity()

  63. What is Weaver? 4. Standalone components class MyFeatureActivity : InjectedFragmentActivity()

    <FrameLayout android:id="@+id/my_layout" ... > <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout>
  64. What is Weaver? 4. Standalone components class MyFeatureActivity : InjectedFragmentActivity()

    <FrameLayout android:id="@+id/my_layout" ... > <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout>
  65. What is Weaver? 4. Standalone components class MyFeatureActivity : InjectedFragmentActivity()

    <FrameLayout android:id="@+id/my_layout" ... > <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout>
  66. What is Weaver? 4. Standalone components class MyFeatureActivity : InjectedFragmentActivity()

    <FrameLayout android:id="@+id/my_layout" ... > <tag android:id="@id/weaverComponent" android:value="MyComponent" /> </ FrameLayout> 5. Easy threading
  67. What is Weaver? 5. 6. Painless subscriptions Easy threading

  68. What is Weaver? 5. 6. Painless subscriptions 7. Testing framework

    Easy threading
  69. What is Weaver? 5. 6. Painless subscriptions 7. Testing framework

    8. IDE support Easy threading
  70. What is Weaver? 5. 6. 7. 8. Painless subscriptions Easy

    threading Testing framework IDE support
  71. None
  72. &

  73. & Mental model is the same Instead of having 3

    building blocks, we only need 1 in Compose: the ViewModel.
  74. & Mental model is the same Binders, Views unnecessary To

    provide everything necessary we will need some configuration via CompositionLocal Cohesive entrypoints
  75. & Mental model is the same Binders, Views unnecessary Cohesive

    entrypoints
  76. & val viewModel = weaverViewModel<MyViewModel>()

  77. & val viewModel = weaverViewModel<MyViewModel>() val state by viewModel.watchAsState()

  78. & val viewModel = weaverViewModel<MyViewModel>() val value by viewModel.watchAsState(MyViewState ::

    value) val state by viewModel.watchAsState()
  79. & val viewModel = weaverViewModel<MyViewModel>() val values by viewModel.watchAsState(MyViewState ::

    value1, MyViewState :: value2, …) val (value1, value2, ... ) = values val value by viewModel.watchAsState(MyViewState :
  80. & val viewModel = weaverViewModel<MyViewModel>() viewModel.subscribeEffects { ... } val

    values by viewModel.watchAsState(MyViewState :
  81. &

  82. & @Composable fun FollowButton() { val viewModel = weaverViewModel<FollowViewModel>() val

    isFollowing by viewModel.watchAsState(FollowViewState :: following) FollowButton(isFollowing) { viewModel.toggleFollow() } }
  83. & @Composable fun FollowButton() { val viewModel = weaverViewModel<FollowViewModel>() val

    isFollowing by viewModel.watchAsState(FollowViewState :: following) FollowButton(isFollowing) { viewModel.toggleFollow() } } @Composable fun FollowButton( isFollowing: Boolean, onClick: () -> Unit ) { ... }
  84. & @Composable fun FollowButton() { val viewModel = weaverViewModel<FollowViewModel>() val

    isFollowing by viewModel.watchAsState(FollowViewState :: following) FollowButton(isFollowing) { viewModel.toggleFollow() } } @Composable fun FollowButton( isFollowing: Boolean, onClick: () -> Unit ) { ... }
  85. & @Composable fun FollowButton() { val viewModel = weaverViewModel<FollowViewModel>() val

    isFollowing by viewModel.watchAsState(FollowViewState :: following) FollowButton(isFollowing) { viewModel.toggleFollow() } } @Composable fun FollowButton( isFollowing: Boolean, onClick: () -> Unit ) { ... }
  86. & @Composable fun FollowButton() { val viewModel = weaverViewModel<FollowViewModel>() val

    isFollowing by viewModel.watchAsState(FollowViewState :: following) FollowButton(isFollowing) { viewModel.toggleFollow() } } @Composable fun FollowButton( isFollowing: Boolean, onClick: () -> Unit ) { ... }
  87. & @Composable fun FollowButton() { val viewModel = weaverViewModel<FollowViewModel>() val

    isFollowing by viewModel.watchAsState(FollowViewState :: following) FollowButton(isFollowing) { viewModel.toggleFollow() } } @Composable fun FollowButton( isFollowing: Boolean, onClick: () -> Unit ) { ... }
  88. &

  89. & strategy 1. Proof of concept 2. Prepare the build

    to make sure that Compose or the new IR backend properly built on CI.
  90. & strategy 2. Adding to the build 3. Design system

    Robust theming and ways to customize for special cases.
  91. & strategy 3. Design system 4. Early Access Program Partner

    up with select feature teams of different needs and allow them to early adopt Compose. Provide hands on support to them. They will find all the possible ways your infra is broken and how your APIs suck. Fix, document.
  92. & strategy 4. Early Access Program 5. Hype train Education

    is king on such a big change. Make sure to communicate internally about Compose, with comprehensive documentation and education sessions / talks / codelabs.
  93. & strategy 5. Hype train 6. Broad adoption Open the

    floodgates for more feature teams to use it.
  94. & strategy 6. Broad adoption 7. Long term support Team

    ownership on upgrading the Compose dependencies. Integration and instrumentation testing are key to make sure the core infra doesn’t break between updates. [CHIRPBIRDI CO N] feature devs
  95. & strategy 7. Long term support

  96. adoption

  97. adoption

  98. adoption New screens

  99. adoption New screens Existing trivial screens

  100. adoption New screens Existing trivial screens

  101. adoption

  102. @Composable fun TopicsHeader() { val viewModel = weaverViewModel<TopicHeaderViewModel>() val state

    by viewModel.watchAsState() Title(state.title) Subtitle(state.subtitle) Row { Facepile(state.users) FollowButton(state.following) { viewModel.toggleFollow() } } Description(state.description) Timeline(state.timeline) }
  103. @Composable fun TopicsHeader() { val viewModel = weaverViewModel<TopicHeaderViewModel>() val state

    by viewModel.watchAsState() Title(state.title) Subtitle(state.subtitle) Row { Facepile(state.users) FollowButton(state.following) { viewModel.toggleFollow() } } Description(state.description) Timeline(state.timeline) }
  104. @Composable fun TopicsHeader() { val viewModel = weaverViewModel<TopicHeaderViewModel>() val state

    by viewModel.watchAsState() Title(state.title) Subtitle(state.subtitle) Row { Facepile(state.users) FollowButton(state.following) { viewModel.toggleFollow() } } Description(state.description) Timeline(state.timeline) } Business logic is reused
  105. @Composable fun TopicsHeader() { val viewModel = weaverViewModel<TopicHeaderViewModel>() val state

    by viewModel.watchAsState() Title(state.title) Subtitle(state.subtitle) Row { Facepile(state.users) FollowButton(state.following) { viewModel.toggleFollow() } } Description(state.description) Timeline(state.timeline) }
  106. @Composable fun TopicsHeader() { val viewModel = weaverViewModel<TopicHeaderViewModel>() val state

    by viewModel.watchAsState() Title(state.title) Subtitle(state.subtitle) Row { Facepile(state.users) FollowButton(state.following) { viewModel.toggleFollow() } } Description(state.description) Timeline(state.timeline) }
  107. @Composable fun Facepile(twitterUsers: List<TwitterUser>, membersClicked: () -> Unit) { val

    borderColor = HorizonTheme.colors.primary.toArgb() AndroidView( :: FacePileView) { facePileView: FacePileView -> facePileView.avatarBorderColor = borderColor facePileView.initFaces(twitterUsers.take(3).reversed()) facePileView.setOnClickListener { membersClicked() } } }
  108. @Composable fun Facepile(twitterUsers: List<TwitterUser>, membersClicked: () -> Unit) { val

    borderColor = HorizonTheme.colors.primary.toArgb() AndroidView( :: FacePileView) { facePileView: FacePileView -> facePileView.avatarBorderColor = borderColor facePileView.initFaces(twitterUsers.take(3).reversed()) facePileView.setOnClickListener { membersClicked() } } }
  109. Can be migrated later on @Composable fun Facepile(twitterUsers: List<TwitterUser>, membersClicked:

    () >
  110. adoption Complex screens

  111. adoption Complex screens Legacy screens

  112. adoption Complex screens Legacy screens

  113. adoption

  114. android:id="@+id/send_dm" style="@style/TwitterButtonMediumBorderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/send_dm" /> android:id="@+id/follow" style="@style/TwitterButtonMediumBorderless" android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:text="@string/follow" /> <com.twitter.ui.components.button.TwitterButton <com.twitter.ui.components.button.TwitterButton
  115. android:id="@+id/send_dm" style="@style/TwitterButtonMediumBorderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/send_dm" /> android:id="@+id/follow" style="@style/TwitterButtonMediumBorderless" android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:text="@string/follow" /> <com.twitter.ui.components.button.TwitterButton <com.twitter.ui.components.button.TwitterButton
  116. android:id="@+id/send_dm" style="@style/TwitterButtonMediumBorderless" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/send_dm" /> android:id="@+id/follow" style="@style/TwitterButtonMediumBorderless" android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:text="@string/follow" /> <com.twitter.ui.components.button. <com.twitter.ui.components.button.HorizonComposeButton HorizonComposeButton
  117. class @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr:

    Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr), HorizonButton { HorizonComposeButton }
  118. class @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr:

    Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr), HorizonButton { HorizonComposeButton }
  119. class @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr:

    Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr), HorizonButton { HorizonComposeButton } private var onClick by mutableStateOf({}) private var buttonText: CharSequence? by mutableStateOf("") override fun getText(): CharSequence? = buttonText override fun setText(value: CharSequence?) { buttonText = value } // ...
  120. class @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr:

    Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr), HorizonButton { HorizonComposeButton } @Composable override fun Content() { HorizonTheme { HorizonButton(_text, onClick, ... ) } } // ...
  121. private val styleButtonAppearanceMap by lazy { fun bold(size: ButtonSize) =

    ButtonStyle(size, ButtonType.Outlined, ButtonColors.Legacy.Primary) fun heavy(size: ButtonSize) = ButtonStyle(size, ButtonType.Filled, ButtonColors.Legacy.Primary) fun regular(size: ButtonSize) = ButtonStyle(size, ButtonType.Outlined, ButtonColors.Legacy.Regular) // ... mapOf( R.style.TwitterButtonLargeBold to bold(ButtonSize.Legacy.Large()), R.style.TwitterButtonSmallBold to bold(ButtonSize.Legacy.Small()), // ... ) } internal fun styleToButtonAppearance(@StyleRes styleResId: Int): ButtonStyle = styleButtonAppearanceMap[styleResId] ?: error("There is no mapping for this legacy style.")
  122. private val styleButtonAppearanceMap by lazy { fun bold(size: ButtonSize) =

    ButtonStyle(size, ButtonType.Outlined, ButtonColors.Legacy.Primary) fun heavy(size: ButtonSize) = ButtonStyle(size, ButtonType.Filled, ButtonColors.Legacy.Primary) fun regular(size: ButtonSize) = ButtonStyle(size, ButtonType.Outlined, ButtonColors.Legacy.Regular) // ... mapOf( R.style.TwitterButtonLargeBold to bold(ButtonSize.Legacy.Large()), R.style.TwitterButtonSmallBold to bold(ButtonSize.Legacy.Small()), // ... ) } internal fun styleToButtonAppearance(@StyleRes styleResId: Int): ButtonStyle = styleButtonAppearanceMap[styleResId] ?: error("There is no mapping for this legacy style.")
  123. <com.twitter.ui.components.button.TwitterButton <com.twitter.ui.components.button.HorizonComposeButton

  124. <com.twitter.ui.components.button.TwitterButton <com.twitter.ui.components.button.HorizonComposeButton

  125. <com.twitter.ui.components.button.TwitterButton <com.twitter.ui.components.button.HorizonComposeButton

  126. <com.twitter.ui.components.button.TwitterButton <com.twitter.ui.components.button.HorizonComposeButton XML won’t help here

  127. AppCompatView Inflater to automatically switch views

  128. <style name="SharedTheme" parent="ThemeBase"> <item name="viewInflaterClass">com.twitter.ui.CustomViewInflater </ item> ... </ style>

  129. <style name="SharedTheme" parent="ThemeBase"> <item name="viewInflaterClass">com.twitter.ui.CustomViewInflater </ item> ... </ style>

    class CustomViewInflater : AppCompatViewInflater() { override fun createView(context: Context, name: String, attrs: AttributeSet): View { if ( name == TwitterButton :: class.java.canonicalName && HorizonComposeButtonUtils.isComposeButtonEnabled() ) { HorizonComposeButton(context, name, attrs) } else { super.createView(context, name, attrs) } } }
  130. <style name="SharedTheme" parent="ThemeBase"> <item name="viewInflaterClass">com.twitter.ui.CustomViewInflater </ item> ... </ style>

    class CustomViewInflater : AppCompatViewInflater() { override fun createView(context: Context, name: String, attrs: AttributeSet): View { if ( name == TwitterButton :: class.java.canonicalName && HorizonComposeButtonUtils.isComposeButtonEnabled() ) { HorizonComposeButton(context, name, attrs) } else { super.createView(context, name, attrs) } } }
  131. class CustomViewInflater : AppCompatViewInflater() { override fun createView(context: Context, name:

    String, attrs: AttributeSet): View { if ( name == TwitterButton :: class.java.canonicalName && HorizonComposeButtonUtils.isComposeButtonEnabled() ) { HorizonComposeButton(context, name, attrs) } else { super.createView(context, name, attrs) } } } <style name="SharedTheme" parent="ThemeBase"> <item name="viewInflaterClass">com.twitter.ui.CustomViewInflater </ item> ... </ style>
  132. class CustomViewInflater : AppCompatViewInflater() { override fun createView(context: Context, name:

    String, attrs: AttributeSet): View { if ( name == TwitterButton :: class.java.canonicalName && HorizonComposeButtonUtils.isComposeButtonEnabled() ) { HorizonComposeButton(context, name, attrs) } else { super.createView(context, name, attrs) } } } But what if we want to be selective?
  133. class CustomViewInflater : AppCompatViewInflater() { override fun createView(context: Context, name:

    String, attrs: AttributeSet): View { if ( name == TwitterButton :: class.java.canonicalName && HorizonComposeButtonUtils.isComposeButtonEnabled() ) { HorizonComposeButton(context, name, attrs) } else { super.createView(context, name, attrs) } } } But what if we want to be selective?
  134. class CustomViewInflater : AppCompatViewInflater() { override fun createView(context: Context, name:

    String, attrs: AttributeSet): View { if ( name == && HorizonComposeButtonUtils.isComposeButtonEnabled() ) { HorizonComposeButton(context, name, attrs) } else { super.createView(context, name, attrs) } } } But what if we want to be selective? MigrationTwitterButton :: class.java.canonicalName
  135. if ( name == && HorizonComposeButtonUtils.isComposeButtonEnabled() ) { HorizonComposeButton(context, name,

    attrs) } else { super.createView(context, name, attrs) } } } But what if we want to be selective? MigrationTwitterButton :: class.java.canonicalName class MigrationTwitterButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : TwitterButton(context, attrs, defStyleAttr)
  136. But what if we want to be selective? <com.twitter.ui.components.button.TwitterButton <com.twitter.ui.components.button.MigrationTwitterButton

    class MigrationTwitterButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : TwitterButton(context, attrs, defStyleAttr)
  137. Use a different subclass of the UI element for what

    you want to migrate
  138. val button = context.findViewById< button.setText(R.string.my_new_fancy_text) TwitterButton>()

  139. val button = context.findViewById< button.setText(R.string.my_new_fancy_text) HorizonButton>()

  140. val button = context.findViewById< button.setText(R.string.my_new_fancy_text) HorizonButton>() class HorizonComposeButton @JvmOverloads constructor(

    ... ) : AbstractComposeView(context, attrs, defStyleAttr), HorizonButton class TwitterButton @JvmOverloads constructor( ... ) : AppCompatButton(context, attrs, defStyleAttr), HorizonButton
  141. val button = context.findViewById< button.setText(R.string.my_new_fancy_text) HorizonButton>() class HorizonComposeButton @JvmOverloads constructor(

    ... ) : AbstractComposeView(context, attrs, defStyleAttr), HorizonButton class TwitterButton @JvmOverloads constructor( ... ) : AppCompatButton(context, attrs, defStyleAttr), HorizonButton
  142. None
  143. None
  144. public interface { // Omit nullity annotation to maintain compatibility

    with TextView's getText() // that returns platform type CharSequence getText(); void setText(@Nullable CharSequence charSequence); void setIcon(@DrawableRes int iconDrawable); boolean isEnabled(); void setEnabled(boolean enabled); int getVisibility(); void setVisibility(int visibility); void setOnClickListener(@Nullable View.OnClickListener listener); void setTag(@Nullable Object action); void setContentDescription(@Nullable CharSequence contentDescription); boolean callOnClick(); HorizonButton }
  145. public interface { HorizonButton }

  146. Programmatic usages: create a common interface

  147. class HorizonComposeButton( ... ) : AbstractComposeView( ... ), HorizonButton {

    var text = mutableStateOf("") init { context.withStyledAttributes(attrs, R.styleable.HorizonComposeButton, defAttrs) { text = getString(R.styleable.HorizonComposeButton_android_text) ?: "" } } Text(text) } } @Composable override fun Content() {
  148. Text(text) } } class HorizonComposeButton( ... ) : AbstractComposeView( ...

    ), HorizonButton { var text = mutableStateOf("") init { context.withStyledAttributes(attrs, R.styleable.HorizonComposeButton, defAttrs) { text = getString(R.styleable.HorizonComposeButton_android_text) ?: "" } } @Composable override fun Content() {
  149. Text(text) } } class HorizonComposeButton( ... ) : AbstractComposeView( ...

    ), HorizonButton { @Composable override fun Content() { val viewModel = weaverViewModel<HorizonButtonViewModel>(id) val text by viewModel.watchAsState(HorizonButtonState :: text)
  150. } } class HorizonComposeButton( ... ) : AbstractComposeView( ... ),

    HorizonButton { var text = mutableStateOf("") init { context.withStyledAttributes(attrs, R.styleable.HorizonComposeButton, defAttrs) { text = getString(R.styleable.HorizonComposeButton_android_text) ?: "" } } @Composable override fun Content() { val internalText = rememberSaveable { text } Text(internalText)
  151. } } class HorizonComposeButton( ... ) : AbstractComposeView( ... ),

    HorizonButton { var text = mutableStateOf("") init { context.withStyledAttributes(attrs, R.styleable.HorizonComposeButton, defAttrs) { text = getString(R.styleable.HorizonComposeButton_android_text) ?: "" } } @Composable override fun Content() { val internalText = rememberSaveable { text } Text(internalText)
  152. } } class HorizonComposeButton( ... ) : AbstractComposeView( ... ),

    HorizonButton { var text = mutableStateOf("") init { context.withStyledAttributes(attrs, R.styleable.HorizonComposeButton, defAttrs) { text = getString(R.styleable.HorizonComposeButton_android_text) ?: "" } } @Composable override fun Content() { val internalText = rememberSaveable { text } Text(internalText) SideEffect { if (textInternal != text) { text = textInternal } }
  153. Preserve state when the AbstractCompose View is recreated

  154. None
  155. Wrapping up!

  156. Wrapping up!

  157. Wrapping up!

  158. Wrapping up!

  159. Question time! #360AnDev Nacho Lopez @mrmans0n 6:10PM · July 23,

    2021
  160. Thank You