Slide 1

Slide 1 text

@marcoGomier @stewemetal Marco Gomiero Adopting Jetpack Compose Safely πŸ‘¨πŸ’» Senior Android Engineer @ TIER 
 Google Developer Expert for Kotlin πŸ‘¨πŸ’» Senior Android Engineer @ TIER 
 Co-organizer of Kotlin Budapest IstvΓ‘n Juhos

Slide 2

Slide 2 text

@marcoGomier @stewemetal Compose in the wild

Slide 3

Slide 3 text

@marcoGomier @stewemetal WHY?

Slide 4

Slide 4 text

@marcoGomier @stewemetal No more 
 RecyclerView Adapters πŸ₯³

Slide 5

Slide 5 text

@marcoGomier @stewemetal Why Compose? πŸ€“ Less code 😎 More fun 😍 Kotlin

Slide 6

Slide 6 text

@marcoGomier @stewemetal Start using Compose

Slide 7

Slide 7 text

@marcoGomier @stewemetal 🚧 β›” Start using Compose

Slide 8

Slide 8 text

@marcoGomier @stewemetal Pre-Compose Architecture

Slide 9

Slide 9 text

@marcoGomier @stewemetal internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay }γ…€ interface Output { val articleNotFound: PublishRelay val articleDetailsUIState: BehaviorRelay }γ…€ val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things title = "Title" output.state.accept(aState) }, { error -> output.hideTopBanner.accept(Unit) }, ) .disposeOnPause() }γ…€ } Pre-Compose ViewModel

Slide 10

Slide 10 text

@marcoGomier @stewemetal internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay }γ…€ interface Output { val articleNotFound: PublishRelay val articleDetailsUIState: BehaviorRelay }γ…€ val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things title = "Title" output.state.accept(aState) }, { error -> output.hideTopBanner.accept(Unit) }, ) .disposeOnPause() } }

Slide 11

Slide 11 text

@marcoGomier @stewemetal internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay }γ…€ interface Output { val articleNotFound: PublishRelay val articleDetailsUIState: BehaviorRelay }γ…€ val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things title = "Title" output.state.accept(aState) }, { error -> output.hideTopBanner.accept(Unit) }, ) .disposeOnPause() } } Data Binding πŸ’€ ● UI State in bindable properties ● Custom binding adapters ● Business Logic in the XML ● Hard to test ● Random tooling issues ● KAPT @marcoGomier @stewemetal

Slide 12

Slide 12 text

@marcoGomier @stewemetal internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay }γ…€ interface Output { val articleNotFound: PublishRelay val articleDetailsUIState: BehaviorRelay }γ…€ val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things

Slide 13

Slide 13 text

@marcoGomier @stewemetal internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay }γ…€ interface Output { val articleNotFound: PublishRelay val articleDetailsUIState: BehaviorRelay }γ…€ val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things title = "Title" output.state.accept(aState) }, { error -> output.hideTopBanner.accept(Unit) }, ) .disposeOnPause() }γ…€ } Legacy ViewModel Multiple Rx Relays ● Data Input ● Events, Navigation and (some) state @marcoGomier @stewemetal

Slide 14

Slide 14 text

@marcoGomier @stewemetal @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) ... interface Input { val articleId: PublishRelay }γ…€ interface Output { val articleNotFound: PublishRelay val articleDetailsUIState: BehaviorRelay }γ…€ val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things title = "Title" output.state.accept(aState) }, { error -> output.hideTopBanner.accept(Unit) }, ) .disposeOnPause() }γ…€ } ● UI State split into multiple places ● Rx specific boilerplate all over the place @marcoGomier @stewemetal

Slide 15

Slide 15 text

@marcoGomier @stewemetal Just switching to Compose is impossible 😞

Slide 16

Slide 16 text

@marcoGomier @stewemetal Untangling the Spaghetti 🍝

Slide 17

Slide 17 text

@marcoGomier @stewemetal Untangling the spaghetti ● Start with a clean slate ● Make adoption as easy as possible ● Consider MAD Architecture suggestions

Slide 18

Slide 18 text

@marcoGomier @stewemetal Untangling the spaghetti https://goo.gle/MAD ● Start with a clean slate ● Make adoption as easy as possible ● Consider MAD Architecture suggestions

Slide 19

Slide 19 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } }

Slide 20

Slide 20 text

@marcoGomier @stewemetal internal class ArticleDetailsViewModel( ... ) : BaseViewModel() { @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } } Untangling the spaghetti - UI state, UI events

Slide 21

Slide 21 text

@marcoGomier @stewemetal internal class ArticleDetailsViewModel( ... ) : BaseViewModel() {​ @get:Bindable var title: String by binding("", BR.title) @get:Bindable var price: String by binding("", BR.price) val input = object : Input { override val articleId = PublishRelay.create() } val output = object : Output { override val articleNotFound = PublishRelay.create() override val state = BehaviorRelay.create() } }​ Untangling the spaghetti - UI state, UI events

Slide 22

Slide 22 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events internal class ArticleDetailsViewModel( ... ) : BaseViewModel() {​ ... }​

Slide 23

Slide 23 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events internal class ArticleDetailsViewModel( ... ) : BaseViewModel() {​ ... }​ https://goo.gle/architecture-state-holders

Slide 24

Slide 24 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { ... }​

Slide 25

Slide 25 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events abstract class BaseTierViewModel <​ Stat e​ >( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) ... }​

Slide 26

Slide 26 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events abstract class BaseTierViewModel <​ Stat e​ >( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable = state.hide() ... }​

Slide 27

Slide 27 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable = state.hide() ... } sealed class ArticleDetailsUIState { object Loading: ArticleDetailsUIState() data ​ class Content( val title: String, val price: String, val articleDetailsEvent: ArticleDetailsEvent? = null, ): ArticleDetailsUIState() }

Slide 28

Slide 28 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable = state.hide() ... } data class ArticleDetailsUIState( val isLoading: Boolean = true, val title: String, val price: String, val articleDetailsEvent: ArticleDetailsEvent? = null, )

Slide 29

Slide 29 text

@marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable = state.hide() ... }

Slide 30

Slide 30 text

@marcoGomier @stewemetal Untangling the spaghetti - User events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { ... }

Slide 31

Slide 31 text

@marcoGomier @stewemetal Untangling the spaghetti - User events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { ... } https:/goo.gle/architecture-ui-events

Slide 32

Slide 32 text

@marcoGomier @stewemetal Untangling the spaghetti - User events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { ... } https:/goo.gle/architecture-ui-events

Slide 33

Slide 33 text

@marcoGomier @stewemetal Untangling the spaghetti - User events abstract class BaseTierViewModel( val initialState: State, ) : ViewModel() { ... }

Slide 34

Slide 34 text

@marcoGomier @stewemetal Untangling the spaghetti - User events abstract class BaseTierViewModel <​ ViewEvent, Stat e​ >( val initialState: State, ) : ViewModel() { ... private val events = PublishSubject.create().toSerialized() ... }​

Slide 35

Slide 35 text

@marcoGomier @stewemetal Untangling the spaghetti - User events abstract class BaseTierViewModel <​ ViewEvent, Stat e​ >( val initialState: State, ) : ViewModel() { ... private val events = PublishSubject.create().toSerialized() protected abstract fun onViewEvent( event: ViewEvent, ) ... }​

Slide 36

Slide 36 text

@marcoGomier @stewemetal Untangling the spaghetti - User events abstract class BaseTierViewModel <​ ViewEvent, Stat e​ >( val initialState: State, ) : ViewModel() { ... private val events = PublishSubject.create().toSerialized() protected abstract fun onViewEvent( event: ViewEvent, ) fu n​ triggerViewEvent(event: ViewEvent) { events.onNext(event) } }​

Slide 37

Slide 37 text

@marcoGomier @stewemetal Untangling the spaghetti - User events val input = object : Input { override val articleId = PublishRelay.create() }​​​​

Slide 38

Slide 38 text

@marcoGomier @stewemetal Untangling the spaghetti - User events val input = object : Input { override val articleId = PublishRelay.create() }​​​​

Slide 39

Slide 39 text

@marcoGomier @stewemetal Untangling the spaghetti - User events val input = object : Input { override val articleId = PublishRelay.create() }​​​​

Slide 40

Slide 40 text

@marcoGomier @stewemetal Untangling the spaghetti - User events val input = object : Input { override val articleId = PublishRelay.create() }​​​​

Slide 41

Slide 41 text

@marcoGomier @stewemetal Untangling the spaghetti - ViewModel testing @Test fun `ArticleDetailsViewModel Test`() {​ val testObserver = viewModel.state().test() every { shopUseCase.getArticle(articleId) }​ returns Single.just(defaultArticle) viewModel.onViewCreated() viewModel.triggerViewEvent(RenderArticle(articleId)) viewModel.onViewResumed() testObserver.assertValueAt(2) { state -> // … }​ }​

Slide 42

Slide 42 text

@marcoGomier @stewemetal @Test fun `ArticleDetailsViewModel Test`() {​ val testObserver = viewModel.state().test() every { shopUseCase.getArticle(articleId) }​ returns Single.just(defaultArticle) viewModel.onViewCreated() viewModel.triggerViewEvent(RenderArticle(articleId)) viewModel.onViewResumed() testObserver.assertValueAt(2) { state -> // … }​ }​ Untangling the spaghetti - ViewModel testing

Slide 43

Slide 43 text

@marcoGomier @stewemetal Untangling the spaghetti - ViewModel testing @Test fun `ArticleDetailsViewModel Test`() {​ val testObserver = viewModel.state().test() every { shopUseCase.getArticle(articleId) }​ returns Single.just(defaultArticle) viewModel.onViewCreated() viewModel.triggerViewEvent(RenderArticle(articleId)) viewModel.onViewResumed() testObserver.assertValueAt(2) { state -> // … }​ }​

Slide 44

Slide 44 text

@marcoGomier @stewemetal Untangling the spaghetti - ViewModel testing @Test fun `ArticleDetailsViewModel Test`() {​ val testObserver = viewModel.state().test() every { shopUseCase.getArticle(articleId) }​ returns Single.just(defaultArticle) viewModel.onViewCreated() viewModel.triggerViewEvent(RenderArticle(articleId)) viewModel.onViewResumed() testObserver.assertValueAt(2) { state -> // … } }​

Slide 45

Slide 45 text

@marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class BaseComposeActivity : AppCompatActivity() { abstract val viewModel: TierViewModel abstract val content: @Composable (State) -> Unit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val state by viewModel.state() .subscribeAsState(initial = viewModel.initialState) content(state) } viewModel.onViewCreated() } }

Slide 46

Slide 46 text

@marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class BaseComposeActivity : AppCompatActivity() { abstract val viewModel: TierViewModel abstract val content: @Composable (State) -> Unit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val state by viewModel.state() .subscribeAsState(initial = viewModel.initialState) content(state) } viewModel.onViewCreated() } }

Slide 47

Slide 47 text

@marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class BaseComposeActivity : AppCompatActivity() { abstract val viewModel: TierViewModel abstract val content: @Composable (State) -> Unit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val state by viewModel.state() .subscribeAsState(initial = viewModel.initialState) content(state) } viewModel.onViewCreated() } }

Slide 48

Slide 48 text

@marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class BaseComposeActivity : AppCompatActivity() { abstract val viewModel: TierViewModel abstract val content: @Composable (State) -> Unit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val state by viewModel.state() .subscribeAsState(initial = viewModel.initialState) content(state) } viewModel.onViewCreated() } }

Slide 49

Slide 49 text

@marcoGomier @stewemetal Untangling the spaghetti - Compose UI internal class ArticleDetailsActivity : BaseComposeActivity() { ... override val content = @Composable { state: ArticleDetailsState -> ArticleDetailsScreen( state = state, onBuyButtonClick = { viewModel.triggerViewEvent(BuyClicked) }, ) } override fun onCreate(savedInstanceState: Bundle?) { … viewModel.triggerViewEvent(RenderArticle(val articleId: String)) }

Slide 50

Slide 50 text

@marcoGomier @stewemetal Untangling the spaghetti - Compose UI internal class ArticleDetailsActivity : BaseComposeActivity() { ... override val content = @Composable { state: ArticleDetailsState -> ArticleDetailsScreen( state = state, onBuyButtonClick = { viewModel.triggerViewEvent(BuyClicked) }, ) } override fun onCreate(savedInstanceState: Bundle?) { … viewModel.triggerViewEvent(RenderArticle(val articleId: String)) }

Slide 51

Slide 51 text

@marcoGomier @stewemetal Untangling the spaghetti - Compose UI internal class ArticleDetailsActivity : BaseComposeActivity() { ... override val content = @Composable { state: ArticleDetailsState -> ArticleDetailsScreen( state = state, onBuyButtonClick = { viewModel.triggerViewEvent(BuyClicked) }, ) } override fun onCreate(savedInstanceState: Bundle?) { … viewModel.triggerViewEvent(RenderArticle(articleId)) }

Slide 52

Slide 52 text

@marcoGomier @stewemetal Gradle Configuration

Slide 53

Slide 53 text

@marcoGomier @stewemetal Gradle Configuration ● Enable Compose only in some modules ● Enable and add Compose libraries with one line

Slide 54

Slide 54 text

@marcoGomier @stewemetal Module Configuration β†’ buildSrc fun configureAndroidModule( project: Project, isDataBindingEnabled: Boolean, isComposeEnabled: Boolean = false, ) = project.libraryExtension.run { ... }

Slide 55

Slide 55 text

@marcoGomier @stewemetal Module Configuration β†’ buildSrc fun configureAndroidModule( project: Project, isDataBindingEnabled: Boolean, isComposeEnabled: Boolean = false, ) = project.libraryExtension.run { ... if (isComposeEnabled) { configureCompose(project) } }

Slide 56

Slide 56 text

@marcoGomier @stewemetal Module Configuration β†’ buildSrc private fun BaseExtension.configureCompose(project: Project) { composeOptions.kotlinCompilerExtensionVersion = project.COMPOSE_COMPILER buildFeatures.compose = true project.dependencies { addProvider("implementation", project.COMPOSE_BUNDLE) addProvider("androidTestImplementation", project.COMPOSE_TESTING_BUNDLE) addProvider("debugImplementation", project.COMPOSE_TESTING_MANIFEST) } }

Slide 57

Slide 57 text

@marcoGomier @stewemetal Gradle Version Catalogue [versions] androidx-compose = "1.3.0" androidx-compose-compiler = "1.3.2" [libraries] androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose" } androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "androidx-compose" } [bundles] compose = ["androidx-compose-foundation", "androidx-compose-material", "androidx-compose-runtime-rxjava2", "androidx- compose-ui", "androidx-compose-ui-tooling", "androidx-customlayout-poolingcontainer"] compose-testing = ["androidx-compose-ui-test-junit4"] @marcoGomier @stewemetal gradle/libs.versions.toml ● Gradle Version Catalog for managing dependencies ● Bundles are awesome to make life easier

Slide 58

Slide 58 text

@marcoGomier @stewemetal Module Configuration β†’ build.gradle plugins { id 'com.android.library' id 'kotlin-android' } ModuleConfig.configureAndroidModuleWithCompose(project) dependencies { ... }

Slide 59

Slide 59 text

@marcoGomier @stewemetal Future: Migrate to convention plugins plugins { id 'com.tier.android.library' id 'kotlin-android' } enabledFeatures { compose() } dependencies { ... } plugins { id 'com.tier.android.library' id 'com.tier.android.compose' id 'kotlin-android' } dependencies { ... }

Slide 60

Slide 60 text

@marcoGomier @stewemetal Future: Use Compose BOM

Slide 61

Slide 61 text

@marcoGomier @stewemetal Design System

Slide 62

Slide 62 text

@marcoGomier @stewemetal Octopus Design System ● 100% Views with Data Binding and @BindingAdapters ● Good component APIs & docs ● Easy usage with View-based UI

Slide 63

Slide 63 text

@marcoGomier @stewemetal Octopus Design System ● 100% Views with Data Binding and @BindingAdapters ● Good component APIs & docs ● Easy usage with View-based UI ● Not fun to maintain 😒 OctopusButtonPrimary.kt R.layout.__internal_view_octopus_button R.styleable.OctopusButton R.drawable.__internal_octopus_button_background_primary @marcoGomier @stewemetal

Slide 64

Slide 64 text

@marcoGomier @stewemetal Octopus Design System ● 100% Views with Data Binding and @BindingAdapters ● Good component APIs & docs ● Easy usage with View-based UI ● Not fun to maintain 😒 // We have to disable specific setters to prevent misuse of the view private var areSettersEnabled = false @marcoGomier @stewemetal

Slide 65

Slide 65 text

@marcoGomier @stewemetal Octopus Design System What about Compose? πŸ€”

Slide 66

Slide 66 text

@marcoGomier @stewemetal Octopus Design System ● Possible approaches for us @marcoGomier @stewemetal

Slide 67

Slide 67 text

@marcoGomier @stewemetal Octopus Design System ● Possible approaches for us Views + AndroidView @Composable fun OctopusButtonPrimary( text: String, onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary(context) .apply { setText(text) setOnClickListener { onClick() } ... } }, update = { view -> ... }, ) } @marcoGomier @stewemetal

Slide 68

Slide 68 text

@marcoGomier @stewemetal Octopus Design System ● Possible approaches for us Views + AndroidView @Composable fun OctopusButtonPrimary( text: String, onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary(context) .apply { setText(text) setOnClickListener { onClick() } ... } }, update = { view -> ... }, ) } @marcoGomier @stewemetal

Slide 69

Slide 69 text

@marcoGomier @stewemetal Octopus Design System ● Possible approaches for us @marcoGomier @stewemetal Views + AndroidView @Composable fun OctopusButtonPrimary( text: String, onClick: () -> Unit, ) { AndroidView( factory = { context -> OctopusButtonPrimary(context) .apply { setText(text) setOnClickListener { onClick() } ... } }, update = { view -> ... }, ) }

Slide 70

Slide 70 text

@marcoGomier @stewemetal Octopus Design System ● Possible approaches for us @Composable public fun OctopusButtonPrimary( text: String, modifier: Modifier = Modifier, buttonSize: ButtonSize = NORMAL, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) { Box() { OctopusRippleTheme() { Button() { Text() } if (loading) { OctopusButtonLoader() } } } } @marcoGomier @stewemetal Reimplement component s​ in ​ Compose

Slide 71

Slide 71 text

@marcoGomier @stewemetal Octopus Design System ● Possible approaches for us @Composable public fun OctopusButtonPrimary( text: String, modifier: Modifier = Modifier, buttonSize: ButtonSize = NORMAL, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) { Box() { OctopusRippleTheme() { Button() { Text() } if (loading) { OctopusButtonLoader() } } } } @marcoGomier @stewemetal Reimplement component s​ in ​ Compose

Slide 72

Slide 72 text

@marcoGomier @stewemetal ● The benefits of pure Compose ● Easier to maintain ● No attachments to the legacy Views ● Easy for teams to adopt Octopus Design System 🀩

Slide 73

Slide 73 text

@marcoGomier @stewemetal ● Downsides of our approach ● A second implementation to maintain ● Needs parity with the View components ● Some APIs are still experimental Octopus Design System 🧐

Slide 74

Slide 74 text

@marcoGomier @stewemetal Octopus Design System ● The future - going Compose-first ● Compose implementation behind the Views - ComposeView ● Life with minimal XML, and more Kotlin ❀ ⏩

Slide 75

Slide 75 text

@marcoGomier @stewemetal In-house Compose onboarding

Slide 76

Slide 76 text

@marcoGomier @stewemetal ● Architecture ● Screens ● Octopus theme & components Experiments

Slide 77

Slide 77 text

@marcoGomier @stewemetal ● Architecture ● Screens ● Octopus theme & components Experiments

Slide 78

Slide 78 text

@marcoGomier @stewemetal ● Regular 1-hour knowledge sharing sessions The Android Guild

Slide 79

Slide 79 text

@marcoGomier @stewemetal ● Regular 1-hour knowledge sharing sessions ● A good place to start the onboarding The Android Guild

Slide 80

Slide 80 text

@marcoGomier @stewemetal ● Regular 1-hour knowledge sharing sessions ● A good place to start the onboarding The Android Guild https://www.marcogomiero.com/talks/2022/imperative-vs-declarative-appdevcon/

Slide 81

Slide 81 text

@marcoGomier @stewemetal The Android Guild πŸ™ Composing an Octopus

Slide 82

Slide 82 text

@marcoGomier @stewemetal The Android Guild πŸ™ Composing an Octopus Compose Shorts

Slide 83

Slide 83 text

@marcoGomier @stewemetal The Android Guild πŸ™ Composing an Octopus Compose Shorts

Slide 84

Slide 84 text

@marcoGomier @stewemetal The Android Guild πŸ™ Composing an Octopus Compose Shorts

Slide 85

Slide 85 text

@marcoGomier @stewemetal The Android Guild πŸ™ Composing an Octopus Compose Shorts

Slide 86

Slide 86 text

@marcoGomier @stewemetal The Android Guild πŸ™ Composing an Octopus Compose Shorts

Slide 87

Slide 87 text

@marcoGomier @stewemetal The Android Guild ● Materials in the main repo ● Sessions are recorded @marcoGomier @stewemetal

Slide 88

Slide 88 text

@marcoGomier @stewemetal Conclusions

Slide 89

Slide 89 text

@marcoGomier @stewemetal It dependsTM

Slide 90

Slide 90 text

@marcoGomier @stewemetal Conclusions ● You can’t just start writing Composable functions ● Take time to revisit your architecture ● A design system will help ● Jumping on the Compose Ship Scooter is not a race ● Zero ➑ Hero takes time We’re still here πŸ˜„

Slide 91

Slide 91 text

Resources ● https://developer.android.com/modern-android-development ● https://developer.android.com/topic/architecture/ui-layer/stateholders ● https://developer.android.com/topic/architecture/ui-layer/events ● https://twitter.com/Lojanda/status/1584589111670673409 ● https://github.com/android/nowinandroid/tree/main/build-logic ● https://www.droidcon.com/2022/06/28/branching-out-to-jetpack- compose/ ● https://github.com/androidx/androidx/blob/androidx-main/compose/docs/ compose-api-guidelines.md ● https://github.com/twitter/compose-rules

Slide 92

Slide 92 text

@marcoGomier @stewemetal IstvΓ‘n Juhos πŸ‘¨πŸ’» Senior Android Engineer @ TIER 
 Co-organizer of Kotlin Budapest 
 Twitter: @stewemetal 
 Github: stewemetal 
 Mastodon: androiddev.social/@stewemetal 
 Thank you! πŸ‘¨πŸ’» Senior Android Engineer @ TIER 
 Google Developer Expert for Kotlin Twitter: @marcoGomier 
 Github: prof18 
 Website: marcogomiero.com 
 Mastodon: androiddev.social/@marcogom Marco Gomiero