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

Adopting Jetpack Compose Safely

Adopting Jetpack Compose Safely

Presented as a joint talk with @marcoGomier at Advanced Kotlin Dev Day, Amsterdam, 2022.11.24.

István Juhos

November 24, 2022
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 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