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

Adopting Jetpack Compose Safely (Droidcon Berli...

Adopting Jetpack Compose Safely (Droidcon Berlin 2023)

Presented as a joint talk with @marcoGomier at Droidcon Berlin, 2023.07.05.

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 and architectural decisions that such a revolutionary change might disrupt.

In this talk, we will share our journey of integrating Compose into the TIER app while offering practical tips for tackling common challenges that arise when working with large-scale codebases, like:

- Getting rid of Data binding,
- Migrating to a Unidirectional Data Flow architecture,
- Going Compose-first with a design system,
- Enabling Compose in a multimodule world,
- Getting Android engineers on board with the changes

István Juhos

July 05, 2023
Tweet

More Decks by István Juhos

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 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
  3. @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() }ㅤ }
  4. @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
  5. @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 */ } .subscribe( { data -> // do things title = "Title" output.state.accept(aState)
  6. @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 */ } .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
  7. @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 */ } .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
  8. @marcoGomier @stewemetal Untangling the spaghetti • Start with a clean

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

    clean slate • Make adoption as easy as possible • Consider MAD Architecture suggestions
  10. @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>() } }
  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) 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
  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) 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
  13. @marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events

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

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

    abstract class BaseTierViewModel <​ Stat e​ >( v a​ l initialState: State, ) : ViewModel() { ... }​
  16. @marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events

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

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

    abstract class BaseTierViewModel <​ Stat e​ >( v a​ l initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable<State> = state.hide() ... }​ data cla s​ s ArticleDetailsUIStat e​ ( v​ al isLoading: Boolean = true, v​ al title: String, v​ al price: String, v a​ l articleDetailsEvent: ArticleDetailsEvent? = null, )
  19. @marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events

    abstract class BaseTierViewModel <​ Stat e​ >( v a​ l initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable<State> = state.hide() ... }​ data cla s​ s ArticleDetailsUIStat e​ ( v​ al isLoading: Boolean = true, v​ al title: String, v​ al price: String, v a​ l articleDetailsEvent: ArticleDetailsEvent? = null, )
  20. @marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events

    data cla s​ s ArticleDetailsUIStat e​ ( v​ al isLoading: Boolean = true, v​ al title: String, v​ al price: String, v a​ l articleDetailsEvent: ArticleDetailsEvent? = null, ) abstract class BaseTierViewModel <​ Stat e​ >( v a​ l initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) fun state(): Observable<State> = state.hide() ... }​
  21. @marcoGomier @stewemetal Untangling the spaghetti - View events abstract class

    BaseTierViewModel <​ Stat e​ >( val initialState: State, ) : ViewModel() { ... }
  22. @marcoGomier @stewemetal Untangling the spaghetti - View events abstract class

    BaseTierViewModel <​ Stat e​ >( val initialState: State, ) : ViewModel() { ... } goo.gle/architecture-ui-events
  23. @marcoGomier @stewemetal Untangling the spaghetti - View events abstract class

    BaseTierViewModel <​ Stat e​ >( val initialState: State, ) : ViewModel() { ... } goo.gle/architecture-ui-events
  24. @marcoGomier @stewemetal Untangling the spaghetti - View events abstract class

    BaseTierViewModel<ViewEvent, Stat e​ >( val initialState: State, ) : ViewModel() { ... }​ goo.gle/architecture-ui-events
  25. @marcoGomier @stewemetal Untangling the spaghetti - View events abstract class

    BaseTierViewModel<ViewEvent, Stat e​ >( val initialState: State, ) : ViewModel() { ... }​ goo.gle/architecture-ui-events ViewEvent = User + UI logic events
  26. @marcoGomier @stewemetal Untangling the spaghetti - View events abstract class

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

    BaseTierViewModel <​ ViewEvent, Stat e​ >( val initialState: State, ) : ViewModel() { ... private val events = PublishSubject.create<ViewEvent>().toSerialized() protected abstract fun onViewEvent( event: ViewEvent, ) ... }​
  28. @marcoGomier @stewemetal Untangling the spaghetti - View 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) } }​
  29. @marcoGomier @stewemetal Untangling the spaghetti - View events val input

    = object : Input { override val articleId = PublishRelay.create<String>() }​​​​ <Button android:onClick="@{ () -> vm.onBuyClicked() }” /> 💀
  30. @marcoGomier @stewemetal Untangling the spaghetti - View 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 }
  31. @marcoGomier @stewemetal Untangling the spaghetti - View 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 }
  32. @marcoGomier @stewemetal Untangling the spaghetti - View 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 }
  33. @marcoGomier @stewemetal Untangling the spaghetti - ViewModel testing @Test fun

    `ArticleDetailsViewModel Test`() {​ val testObserver = viewModel.state().test() }​
  34. @marcoGomier @stewemetal Untangling the spaghetti - ViewModel testing @Test fun

    `ArticleDetailsViewModel Test`() {​ val testObserver = viewModel.state().test() every { shopUseCase.getArticle(articleId) }​ returns Single.just(defaultArticle) }​
  35. @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.triggerViewEvent(RenderArticle(articleId)) }​
  36. @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.triggerViewEvent(RenderArticle(articleId)) testObserver.assertValueAt(2) { state -> // … }​ }​
  37. @marcoGomier @stewemetal • Already established navigation • Activities and Fragments

    • Introducing new things one step at a time • Migration hell ❌ Untangling the spaghetti - Compose UI 🫠
  38. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { }
  39. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstra c​ t v a​ l viewModel: TierViewModel<ViewEvent, State> }
  40. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstra c​ t v a​ l viewModel: TierViewModel<ViewEvent, State> abstract val content: @Composable (State) -> Unit }
  41. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstra c​ t v a​ l viewModel: TierViewModel<ViewEvent, State> abstract val content: @Composable (State) -> Unit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } }
  42. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstra c​ t v a​ l viewModel: TierViewModel<ViewEvent, State> abstract val content: @Composable (State) -> Unit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { } } }
  43. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstra c​ t v a​ l 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) } } }
  44. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI abstract class

    BaseComposeActivity<ViewEvent, State> : AppCompatActivity() { abstra c​ t v a​ l 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) } } }
  45. @marcoGomier @stewemetal Untangling the spaghetti - Compose UI internal class

    ArticleDetailsActivity : BaseComposeActivity<ArticleDetailsViewEvent, ArticleDetailsState>() { ... }
  46. @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) }, ) } }
  47. @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)) } }
  48. @marcoGomier @stewemetal Gradle Configuration • Enable Compose only in some

    modules • Enable and add Compose libraries with one line
  49. @marcoGomier @stewemetal class AndroidLibraryComposeConventionPlugin : Plugin<Project> { override fun apply(target:

    Project) { with(target) { pluginManager.apply("com.android.library") val extension = extensions.getByType<LibraryExtension>() extension.apply { composeOptions.kotlinCompilerExtensionVersion = project.COMPOSE_COMPILER buildFeatures.compose = true dependencies { add("implementation", platform(COMPOSE_BOM)) add("implementation", project.COMPOSE_BUNDLE) add("androidTestImplementation", platform(COMPOSE_BOM)) add("androidTestImplementation", project.COMPOSE_TESTING_BUNDLE) add("debugImplementation", project.COMPOSE_TESTING_MANIFEST) } } } } } Module Configuration → Convention Plugin
  50. @marcoGomier @stewemetal Gradle Version Catalog [versions] androidx-compose = "1.3.0" androidx-compose-compiler

    = "1.3.2" androidx-compose-bom = "2023.05.01" [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" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } [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 (and BOMs) are awesome to make life easier
  51. @marcoGomier @stewemetal Octopus Design System - B.C. • 100% Views

    with Data Binding and @BindingAdapters • Verbose component APIs, but good docs • Easy usage with View-based UI
  52. @marcoGomier @stewemetal Octopus Design System - B.C. • 100% Views

    with Data Binding and @BindingAdapters • Verbose component APIs, but good 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
  53. @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
  54. @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
  55. @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 -> ... }, ) }
  56. @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
  57. @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
  58. @marcoGomier @stewemetal • The benefits of pure Compose • Easier

    to maintain • No attachments to the legacy Views • Easy for teams to adopt Octopus Design System 🤩
  59. @marcoGomier @stewemetal • Downsides of our approach • A second

    implementation to maintain • Keeping them in sync is tedious • New components = twice the work Octopus Design System 🫠
  60. @marcoGomier @stewemetal Octopus Design System - Going Compose-first • Use

    the Compose implementation of components to render the Views • Compose-View interop 💡
  61. @marcoGomier @stewemetal Octopus Design System - Going Compose-first • View

    wrappers using the Compose implementations • Keep original custom attributes working • XML preview support • UI Testing support 🔧
  62. @marcoGomier @stewemetal • Regular 1-hour knowledge sharing sessions • A

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

    good place to start the onboarding The Android Guild marcogomiero.com/talks/2022/imperative-vs-declarative-appdevcon/
  64. @marcoGomier @stewemetal The Android Guild • Materials in the main

    repo • Sessions are recorded @marcoGomier @stewemetal
  65. @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 😄
  66. 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/mrmans0n/compose-rules • https://developer.android.com/build/migrate-to-ksp • https://issuetracker.google.com/issues/173030256 • https://www.istvanjuhos.dev/talks/2023/20230530-composing-a-design-system/
  67. @marcoGomier @stewemetal István Juhos 👨💻 Senior Android Engineer @ TIER

    
  Co-organizer of Kotlin Budapest 
 Twitter: @stewemetal 
 Github: stewemetal 
 Website: istvanjuhos.dev 
 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