$30 off During Our Annual Pro Sale. View Details »

Adopting Jetpack Compose Safely | Advanced Kotlin Dev Day

Adopting Jetpack Compose Safely | Advanced Kotlin Dev Day

If you want to start adopting Jetpack Compose in an existing, large codebase, worked on by multiple teams, you can’t just add the dependency and start creating composables right away.

In such projects, there are already established conventions, processes, and architecture decisions that will be disrupted with such a revolutionary change as Compose, since it requires a mental model shift towards declarative patterns.

In this talk, we will show the process that led us to a harmless integration of Compose into the TIER application, focusing on tooling, architectural changes, the evolution of our design system, and how we managed to get our developers onboard for all of the above.

Talk did together with István Juhos

Marco Gomiero

November 24, 2022
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. @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
  2. @marcoGomier @stewemetal Compose in the wild

  3. @marcoGomier @stewemetal WHY?

  4. @marcoGomier @stewemetal No more 
 RecyclerView Adapters 🥳

  5. @marcoGomier @stewemetal Why Compose? 🤓 Less code 😎 More fun

    😍 Kotlin
  6. @marcoGomier @stewemetal Start using Compose

  7. @marcoGomier @stewemetal 🚧 ⛔ Start using Compose

  8. @marcoGomier @stewemetal Pre-Compose Architecture

  9. @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<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } 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
  10. @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<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } 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() } }
  11. @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<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } 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
  12. @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<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } override fun onViewResumed() { super.onViewResumed() input.articleId .flatMap { /* Get stuff from network */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things
  13. @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<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } 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
  14. @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<String> }ㅤ interface Output { val articleNotFound: PublishRelay<Unit> val articleDetailsUIState: BehaviorRelay<ArticleDetailsUIState> }ㅤ val input = object : Input { override val articleId = PublishRelay.create<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } 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
  15. @marcoGomier @stewemetal Just switching to Compose is impossible 😞

  16. @marcoGomier @stewemetal Untangling the Spaghetti 🍝

  17. @marcoGomier @stewemetal Untangling the spaghetti • Start with a clean

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

    clean slate • Make adoption as easy as possible • Consider MAD Architecture suggestions
  19. @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<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } }
  20. @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<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } } Untangling the spaghetti - UI state, UI events
  21. @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<String>() } val output = object : Output { override val articleNotFound = PublishRelay.create<Unit>() override val state = BehaviorRelay.create<ArticleDetailsUIState>() } }​ Untangling the spaghetti - UI state, UI events
  22. @marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    = object : Input { override val articleId = PublishRelay.create<String>() }​​​​ <Button android:onClick="@{ () -> vm.onBuyClicked() }” /> 💀
  38. @marcoGomier @stewemetal Untangling the spaghetti - User events val input

    = object : Input { override val articleId = PublishRelay.create<String>() }​​​​ <Button android:onClick="@{ () -> vm.onBuyClicked() }” /> sealed interface ArticleDetailsViewEvent { data class RenderArticle( val articleId: String ) : ArticleDetailsViewEvent object BuyClicked: ArticleDetailsViewEvent }
  39. @marcoGomier @stewemetal Untangling the spaghetti - User events val input

    = object : Input { override val articleId = PublishRelay.create<String>() }​​​​ <Button android:onClick="@{ () -> vm.onBuyClicked() }” /> sealed interface ArticleDetailsViewEvent { data class RenderArticle( val articleId: String ) : ArticleDetailsViewEvent object BuyClicked: ArticleDetailsViewEvent }
  40. @marcoGomier @stewemetal Untangling the spaghetti - User events val input

    = object : Input { override val articleId = PublishRelay.create<String>() }​​​​ <Button android:onClick="@{ () -> vm.onBuyClicked() }” /> sealed interface ArticleDetailsViewEvent { data class RenderArticle( val articleId: String ) : ArticleDetailsViewEvent object BuyClicked: ArticleDetailsViewEvent }
  41. @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 -> // … }​ }​
  42. @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
  43. @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 -> // … }​ }​
  44. @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 -> // … } }​
  45. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstract val viewModel: TierViewModel<ViewEvent, State> 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() } }
  46. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstract val viewModel: TierViewModel<ViewEvent, State> 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() } }
  47. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstract val viewModel: TierViewModel<ViewEvent, State> 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() } }
  48. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstract val viewModel: TierViewModel<ViewEvent, State> 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() } }
  49. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI internal class

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

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

    ArticleDetailsActivity : BaseComposeActivity<ArticleDetailsViewEvent, ArticleDetailsState>() { ... override val content = @Composable { state: ArticleDetailsState -> ArticleDetailsScreen( state = state, onBuyButtonClick = { viewModel.triggerViewEvent(BuyClicked) }, ) } override fun onCreate(savedInstanceState: Bundle?) { … viewModel.triggerViewEvent(RenderArticle(articleId)) }
  52. @marcoGomier @stewemetal Gradle Configuration

  53. @marcoGomier @stewemetal Gradle Configuration • Enable Compose only in some

    modules • Enable and add Compose libraries with one line
  54. @marcoGomier @stewemetal Module Configuration → buildSrc fun configureAndroidModule( project: Project,

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

    isDataBindingEnabled: Boolean, isComposeEnabled: Boolean = false, ) = project.libraryExtension.run { ... if (isComposeEnabled) { configureCompose(project) } }
  56. @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) } }
  57. @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
  58. @marcoGomier @stewemetal Module Configuration → build.gradle plugins { id 'com.android.library'

    id 'kotlin-android' } ModuleConfig.configureAndroidModuleWithCompose(project) dependencies { ... }
  59. @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 { ... }
  60. @marcoGomier @stewemetal Future: Use Compose BOM

  61. @marcoGomier @stewemetal Design System

  62. @marcoGomier @stewemetal Octopus Design System • 100% Views with Data

    Binding and @BindingAdapters • Good component APIs & docs • Easy usage with View-based UI
  63. @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
  64. @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
  65. @marcoGomier @stewemetal Octopus Design System What about Compose? 🤔

  66. @marcoGomier @stewemetal Octopus Design System • Possible approaches for us

    @marcoGomier @stewemetal
  67. @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
  68. @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
  69. @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 -> ... }, ) }
  70. @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
  71. @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
  72. @marcoGomier @stewemetal • The benefits of pure Compose • Easier

    to maintain • No attachments to the legacy Views • Easy for teams to adopt Octopus Design System 🤩
  73. @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 🧐
  74. @marcoGomier @stewemetal Octopus Design System • The future - going

    Compose-first • Compose implementation behind the Views - ComposeView • Life with minimal XML, and more Kotlin ❤ ⏩
  75. @marcoGomier @stewemetal In-house Compose onboarding

  76. @marcoGomier @stewemetal • Architecture • Screens • Octopus theme &

    components Experiments
  77. @marcoGomier @stewemetal • Architecture • Screens • Octopus theme &

    components Experiments
  78. @marcoGomier @stewemetal • Regular 1-hour knowledge sharing sessions The Android

    Guild
  79. @marcoGomier @stewemetal • Regular 1-hour knowledge sharing sessions • A

    good place to start the onboarding The Android Guild
  80. @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/
  81. @marcoGomier @stewemetal The Android Guild 🐙 Composing an Octopus

  82. @marcoGomier @stewemetal The Android Guild 🐙 Composing an Octopus Compose

    Shorts
  83. @marcoGomier @stewemetal The Android Guild 🐙 Composing an Octopus Compose

    Shorts
  84. @marcoGomier @stewemetal The Android Guild 🐙 Composing an Octopus Compose

    Shorts
  85. @marcoGomier @stewemetal The Android Guild 🐙 Composing an Octopus Compose

    Shorts
  86. @marcoGomier @stewemetal The Android Guild 🐙 Composing an Octopus Compose

    Shorts
  87. @marcoGomier @stewemetal The Android Guild • Materials in the main

    repo • Sessions are recorded @marcoGomier @stewemetal
  88. @marcoGomier @stewemetal Conclusions

  89. @marcoGomier @stewemetal It dependsTM

  90. @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 😄
  91. 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
  92. @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