Slide 1

Slide 1 text

UI Architecture in the Compose world Android UIs at Scale 360|AnDev 2021

Slide 2

Slide 2 text

Nacho Lopez He/Him Software Engineer Android Core UI @mrmans0n

Slide 3

Slide 3 text

[CHIRP BIRD ICO N] UI Architecture

Slide 4

Slide 4 text

A long time ago in a galaxy far, far away …

Slide 5

Slide 5 text

A long time ago in a galaxy far, far away … Well, actually… it was in 2017

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

It needed to stop

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

19 interface ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable } interface ViewDelegate { protected val rootView: V }

Slide 20

Slide 20 text

20 interface ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable } interface ViewDelegate { protected val rootView: V } But it still was too unopinionated

Slide 21

Slide 21 text

But it still was too unopinionated 3 classes

Slide 22

Slide 22 text

But it still was too unopinionated 3 classes Weak contract

Slide 23

Slide 23 text

But it still was too unopinionated 3 classes Weak contract RxJava++

Slide 24

Slide 24 text

So how did we fix that?

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable } interface ViewDelegate { protected val rootView: V } interface

Slide 29

Slide 29 text

ViewModel interface Binder { fun bind(viewDelegate: ViewDelegate, viewModel: ViewModel): Disposable } interface ViewDelegate { protected val rootView: V } interface

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

ViewModel class MyViewModel : { internal fun textChanged(): Observable = ... } internal fun onButtonClicked(): Observable = button.clicks() }

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Merge all the streams into one to create a stronger contract

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

interface ViewDelegate { protected val rootView: V fun render(state: VS) fun userIntentObservable(): Observable = 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 } }

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

VM MV

Slide 41

Slide 41 text

VM MVI

Slide 42

Slide 42 text

What we use: Weaver [CHIRPBIRDI CO N] ’s MVI + Data binding UI architecture framework

Slide 43

Slide 43 text

What is Weaver?

Slide 44

Slide 44 text

1. Data binding What is Weaver?

Slide 45

Slide 45 text

1. Data binding What is Weaver? FrameLayout>

Slide 46

Slide 46 text

1. Data binding What is Weaver? FrameLayout>

Slide 47

Slide 47 text

1. Data binding What is Weaver? FrameLayout>

Slide 48

Slide 48 text

1. Data binding What is Weaver? FrameLayout>

Slide 49

Slide 49 text

1. Data binding What is Weaver? FrameLayout> 2. MVI

Slide 50

Slide 50 text

2. MVI What is Weaver?

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

What is Weaver? 3. Unidirectional Data Flow

Slide 57

Slide 57 text

What is Weaver? 3. Unidirectional Data Flow

Slide 58

Slide 58 text

What is Weaver? 3. Unidirectional Data Flow

Slide 59

Slide 59 text

What is Weaver? 3. Unidirectional Data Flow

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

4. Standalone components What is Weaver?

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

What is Weaver? 4. Standalone components class MyFeatureActivity : InjectedFragmentActivity() FrameLayout> 5. Easy threading

Slide 67

Slide 67 text

What is Weaver? 5. 6. Painless subscriptions Easy threading

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

&

Slide 73

Slide 73 text

& Mental model is the same Instead of having 3 building blocks, we only need 1 in Compose: the ViewModel.

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

& Mental model is the same Binders, Views unnecessary Cohesive entrypoints

Slide 76

Slide 76 text

& val viewModel = weaverViewModel()

Slide 77

Slide 77 text

& val viewModel = weaverViewModel() val state by viewModel.watchAsState()

Slide 78

Slide 78 text

& val viewModel = weaverViewModel() val value by viewModel.watchAsState(MyViewState :: value) val state by viewModel.watchAsState()

Slide 79

Slide 79 text

& val viewModel = weaverViewModel() val values by viewModel.watchAsState(MyViewState :: value1, MyViewState :: value2, …) val (value1, value2, ... ) = values val value by viewModel.watchAsState(MyViewState :

Slide 80

Slide 80 text

& val viewModel = weaverViewModel() viewModel.subscribeEffects { ... } val values by viewModel.watchAsState(MyViewState :

Slide 81

Slide 81 text

&

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

&

Slide 89

Slide 89 text

& strategy 1. Proof of concept 2. Prepare the build to make sure that Compose or the new IR backend properly built on CI.

Slide 90

Slide 90 text

& strategy 2. Adding to the build 3. Design system Robust theming and ways to customize for special cases.

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

& strategy 5. Hype train 6. Broad adoption Open the floodgates for more feature teams to use it.

Slide 94

Slide 94 text

& 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

Slide 95

Slide 95 text

& strategy 7. Long term support

Slide 96

Slide 96 text

adoption

Slide 97

Slide 97 text

adoption

Slide 98

Slide 98 text

adoption New screens

Slide 99

Slide 99 text

adoption New screens Existing trivial screens

Slide 100

Slide 100 text

adoption New screens Existing trivial screens

Slide 101

Slide 101 text

adoption

Slide 102

Slide 102 text

@Composable fun TopicsHeader() { val viewModel = weaverViewModel() 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) }

Slide 103

Slide 103 text

@Composable fun TopicsHeader() { val viewModel = weaverViewModel() 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) }

Slide 104

Slide 104 text

@Composable fun TopicsHeader() { val viewModel = weaverViewModel() 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

Slide 105

Slide 105 text

@Composable fun TopicsHeader() { val viewModel = weaverViewModel() 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) }

Slide 106

Slide 106 text

@Composable fun TopicsHeader() { val viewModel = weaverViewModel() 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) }

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

Can be migrated later on @Composable fun Facepile(twitterUsers: List, membersClicked: () >

Slide 110

Slide 110 text

adoption Complex screens

Slide 111

Slide 111 text

adoption Complex screens Legacy screens

Slide 112

Slide 112 text

adoption Complex screens Legacy screens

Slide 113

Slide 113 text

adoption

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

Slide 124

Slide 124 text

Slide 125

Slide 125 text

Slide 126

Slide 126 text

Slide 127

Slide 127 text

AppCompatView Inflater to automatically switch views

Slide 128

Slide 128 text

<item name="viewInflaterClass">com.twitter.ui.CustomViewInflater </ item> ... </ style>

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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) } } } <item name="viewInflaterClass">com.twitter.ui.CustomViewInflater </ item> ... </ style>

Slide 132

Slide 132 text

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?

Slide 133

Slide 133 text

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?

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

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)

Slide 136

Slide 136 text

But what if we want to be selective?

Slide 137

Slide 137 text

Use a different subclass of the UI element for what you want to migrate

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

No content

Slide 143

Slide 143 text

No content

Slide 144

Slide 144 text

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 }

Slide 145

Slide 145 text

public interface { HorizonButton }

Slide 146

Slide 146 text

Programmatic usages: create a common interface

Slide 147

Slide 147 text

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() {

Slide 148

Slide 148 text

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() {

Slide 149

Slide 149 text

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

Slide 150

Slide 150 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() { val internalText = rememberSaveable { text } Text(internalText)

Slide 151

Slide 151 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() { val internalText = rememberSaveable { text } Text(internalText)

Slide 152

Slide 152 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() { val internalText = rememberSaveable { text } Text(internalText) SideEffect { if (textInternal != text) { text = textInternal } }

Slide 153

Slide 153 text

Preserve state when the AbstractCompose View is recreated

Slide 154

Slide 154 text

No content

Slide 155

Slide 155 text

Wrapping up!

Slide 156

Slide 156 text

Wrapping up!

Slide 157

Slide 157 text

Wrapping up!

Slide 158

Slide 158 text

Wrapping up!

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

Thank You