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

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 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 */ } .observeOn(schedulerProvider.mainThread()) .subscribeOn(schedulerProvider.io()) .subscribe( { data -> // do things
  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 */ } .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
  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 */ } .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
  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 https://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() {​ ... }​ https://goo.gle/architecture-state-holders
  15. @marcoGomier @stewemetal Untangling the spaghetti - UI state, UI events

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

    abstract class BaseTierViewModel <​ Stat e​ >( val initialState: State, ) : ViewModel() { private val state = BehaviorSubject.createDefault(initialState) ... }​
  17. @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() ... }​
  18. @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() }
  19. @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, )
  20. @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() ... }
  21. @marcoGomier @stewemetal Untangling the spaghetti - User events abstract class

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

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

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

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

    BaseTierViewModel <​ ViewEvent, Stat e​ >( val initialState: State, ) : ViewModel() { ... private val events = PublishSubject.create<ViewEvent>().toSerialized() ... }​
  26. @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, ) ... }​
  27. @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) } }​
  28. @marcoGomier @stewemetal Untangling the spaghetti - User events val input

    = object : Input { override val articleId = PublishRelay.create<String>() }​​​​ <Button android:onClick="@{ () -> vm.onBuyClicked() }” /> 💀
  29. @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 }
  30. @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 }
  31. @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 }
  32. @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 -> // … }​ }​
  33. @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
  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) viewModel.onViewCreated() viewModel.triggerViewEvent(RenderArticle(articleId)) viewModel.onViewResumed() testObserver.assertValueAt(2) { state -> // … }​ }​
  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.onViewCreated() viewModel.triggerViewEvent(RenderArticle(articleId)) viewModel.onViewResumed() testObserver.assertValueAt(2) { state -> // … } }​
  36. @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() } }
  37. @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() } }
  38. @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() } }
  39. @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() } }
  40. @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)) }
  41. @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)) }
  42. @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)) }
  43. @marcoGomier @stewemetal Gradle Configuration • Enable Compose only in some

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

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

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

    id 'kotlin-android' } ModuleConfig.configureAndroidModuleWithCompose(project) dependencies { ... }
  49. @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 { ... }
  50. @marcoGomier @stewemetal Octopus Design System • 100% Views with Data

    Binding and @BindingAdapters • Good component APIs & docs • Easy usage with View-based UI
  51. @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
  52. @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
  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 • Needs parity with the View components • Some APIs are still experimental Octopus Design System 🧐
  60. @marcoGomier @stewemetal Octopus Design System • The future - going

    Compose-first • Compose implementation behind the Views - ComposeView • Life with minimal XML, and more Kotlin ❤ ⏩
  61. @marcoGomier @stewemetal • Regular 1-hour knowledge sharing sessions • A

    good place to start the onboarding The Android Guild
  62. @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/
  63. @marcoGomier @stewemetal The Android Guild • Materials in the main

    repo • Sessions are recorded @marcoGomier @stewemetal
  64. @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 😄
  65. 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
  66. @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