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

Adopting Jetpack Compose Safely | droidcon Lisbon

Marco Gomiero
September 29, 2023

Adopting Jetpack Compose Safely | droidcon Lisbon

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

Marco Gomiero

September 29, 2023
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

    View full-size slide

  2. Compose

    in the wild

    View full-size slide

  3. @marcoGomier @stewemetal
    WHY?

    View full-size slide

  4. @marcoGomier @stewemetal
    No more

    RecyclerView Adapters
    🥳

    View full-size slide

  5. @marcoGomier @stewemetal
    Why Compose?
    🤓 Less code


    😎 More fun


    😍 Kotlin

    View full-size slide

  6. @marcoGomier @stewemetal
    Start using Compose

    View full-size slide

  7. @marcoGomier @stewemetal
    🚧 ⛔ Start using Compose

    View full-size slide

  8. @marcoGomier @stewemetal
    Pre-Compose
    Architecture 🏗

    View full-size slide

  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


    }ㅤ

    interface Output {


    val articleNotFound: PublishRelay


    val articleDetailsUIState: BehaviorRelay


    }ㅤ

    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    override fun onViewResumed() {


    super.onViewResumed()


    input.articleId


    .flatMap { /* Get stuff from network */ }


    .observeOn(schedulerProvider.mainThread())


    .subscribeOn(schedulerProvider.io())


    .subscribe(


    { data ->


    // do things


    title = "Title"


    output.state.accept(aState)


    },


    { error ->


    output.hideTopBanner.accept(Unit)


    },


    )


    .disposeOnPause()


    }ㅤ

    }
    Pre-Compose ViewModel

    View full-size slide

  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


    }ㅤ

    interface Output {


    val articleNotFound: PublishRelay


    val articleDetailsUIState: BehaviorRelay


    }ㅤ

    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    override fun onViewResumed() {


    super.onViewResumed()


    input.articleId


    .flatMap { /* Get stuff from network */ }


    .observeOn(schedulerProvider.mainThread())


    .subscribeOn(schedulerProvider.io())


    .subscribe(


    { data ->


    // do things


    title = "Title"


    output.state.accept(aState)


    },


    { error ->


    output.hideTopBanner.accept(Unit)


    },


    )


    .disposeOnPause()


    }


    }

    View full-size slide

  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


    }ㅤ

    interface Output {


    val articleNotFound: PublishRelay


    val articleDetailsUIState: BehaviorRelay


    }ㅤ

    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    override fun onViewResumed() {


    super.onViewResumed()


    input.articleId


    .flatMap { /* Get stuff from network */ }


    .observeOn(schedulerProvider.mainThread())


    .subscribeOn(schedulerProvider.io())


    .subscribe(


    { data ->


    // do things


    title = "Title"


    output.state.accept(aState)


    },


    { error ->


    output.hideTopBanner.accept(Unit)


    },


    )


    .disposeOnPause()


    }


    }
    Data Binding 💀
    ● UI State in bindable properties


    ● Custom binding adapters


    ● Business Logic in the XML


    ● Hard to test


    ● Random tooling issues


    ● KAPT
    @marcoGomier @stewemetal

    View full-size slide

  12. @marcoGomier @stewemetal
    @marcoGomier @stewemetal
    developer.android.com/build/migrate-to-ksp

    View full-size slide

  13. @marcoGomier @stewemetal
    @marcoGomier @stewemetal
    developer.android.com/build/migrate-to-ksp
    issuetracker.google.com/issues/173030256

    View full-size slide

  14. @marcoGomier @stewemetal
    internal class ArticleDetailsViewModel(


    ...


    ) : BaseViewModel() {


    @get:Bindable var title: String by binding("", BR.title)


    @get:Bindable var price: String by binding("", BR.price)


    ...



    interface Input {


    val articleId: PublishRelay


    }ㅤ

    interface Output {


    val articleNotFound: PublishRelay


    val articleDetailsUIState: BehaviorRelay


    }ㅤ

    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    override fun onViewResumed() {


    super.onViewResumed()


    input.articleId


    .flatMap { /* Get stuff from network */ }


    .subscribe(


    { data ->


    // do things


    title = "Title"


    output.state.accept(aState)




    View full-size slide

  15. @marcoGomier @stewemetal
    internal class ArticleDetailsViewModel(


    ...


    ) : BaseViewModel() {


    @get:Bindable var title: String by binding("", BR.title)


    @get:Bindable var price: String by binding("", BR.price)


    ...




    interface Input {


    val articleId: PublishRelay


    }ㅤ

    interface Output {


    val articleNotFound: PublishRelay


    val articleDetailsUIState: BehaviorRelay


    }ㅤ

    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    override fun onViewResumed() {


    super.onViewResumed()


    input.articleId


    .flatMap { /* Get stuff from network */ }


    .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

    View full-size slide

  16. @marcoGomier @stewemetal
    @get:Bindable var title: String by binding("", BR.title)


    @get:Bindable var price: String by binding("", BR.price)


    ...




    interface Input {


    val articleId: PublishRelay


    }ㅤ

    interface Output {


    val articleNotFound: PublishRelay


    val articleDetailsUIState: BehaviorRelay


    }ㅤ

    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    override fun onViewResumed() {


    super.onViewResumed()


    input.articleId


    .flatMap { /* Get stuff from network */ }


    .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

    View full-size slide

  17. @marcoGomier @stewemetal
    Just switching to
    Compose is impossible
    😞

    View full-size slide

  18. @marcoGomier @stewemetal
    Untangling the
    Spaghetti 🍝

    View full-size slide

  19. @marcoGomier @stewemetal
    Untangling the spaghetti
    ● Start with a clean slate


    ● Make adoption as easy as possible


    ● Consider MAD Architecture suggestions

    View full-size slide

  20. @marcoGomier @stewemetal
    Untangling the spaghetti
    goo.gle/mad
    ● Start with a clean slate


    ● Make adoption as easy as possible


    ● Consider MAD Architecture suggestions

    View full-size slide

  21. @marcoGomier @stewemetal
    Untangling the spaghetti - UI state, UI events
    internal class ArticleDetailsViewModel(


    ...


    ) : BaseViewModel() {


    @get:Bindable var title: String by binding("", BR.title)


    @get:Bindable var price: String by binding("", BR.price)


    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    }


    View full-size slide

  22. @marcoGomier @stewemetal
    internal class ArticleDetailsViewModel(


    ...


    ) : BaseViewModel() {


    @get:Bindable var title: String by binding("", BR.title)


    @get:Bindable var price: String by binding("", BR.price)


    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    }


    Untangling the spaghetti - UI state, UI events

    View full-size slide

  23. @marcoGomier @stewemetal
    internal class ArticleDetailsViewModel(


    ...


    ) : BaseViewModel()
    {​

    @get:Bindable var title: String by binding("", BR.title)


    @get:Bindable var price: String by binding("", BR.price)


    val input = object : Input {


    override val articleId = PublishRelay.create()


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    }​

    Untangling the spaghetti - UI state, UI events

    View full-size slide

  24. @marcoGomier @stewemetal
    Untangling the spaghetti - UI state, UI events
    internal class ArticleDetailsViewModel(


    ...


    ) : BaseViewModel()
    {​

    ...


    }​

    View full-size slide

  25. @marcoGomier @stewemetal
    Untangling the spaghetti - UI state, UI events
    internal class ArticleDetailsViewModel(


    ...


    ) : BaseViewModel()
    {​

    ...


    }​

    goo.gle/architecture-state-holders

    View full-size slide

  26. @marcoGomier @stewemetal
    Untangling the spaghetti - UI state, UI events
    abstract class BaseTierViewModel(


    v
    a​
    l initialState: State,


    ) : ViewModel() {


    ...


    }​

    View full-size slide

  27. @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)


    ...


    }​

    View full-size slide

  28. @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.hide()


    ...


    }​

    View full-size slide

  29. @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.hide()


    ...


    }​

    data cla
    s​
    s ArticleDetailsUIState(


    v​
    al isLoading: Boolean = true,


    v​
    al title: String,


    v​
    al price: String,


    v
    a​
    l articleDetailsEvent: ArticleDetailsEvent? = null,


    )


    View full-size slide

  30. @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.hide()


    ...


    }​

    data cla
    s​
    s ArticleDetailsUIState(


    v​
    al isLoading: Boolean = true,


    v​
    al title: String,


    v​
    al price: String,


    v
    a​
    l articleDetailsEvent: ArticleDetailsEvent? = null,


    )


    View full-size slide

  31. @marcoGomier @stewemetal
    Untangling the spaghetti - UI state, UI events
    data cla
    s​
    s ArticleDetailsUIState(


    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.hide()


    ...


    }​

    View full-size slide

  32. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModel(


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    View full-size slide

  33. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModel(


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    goo.gle/architecture-ui-events

    View full-size slide

  34. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModel(


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    goo.gle/architecture-ui-events

    View full-size slide

  35. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModele​
    >(


    val initialState: State,


    ) : ViewModel() {


    ...


    }​

    goo.gle/architecture-ui-events

    View full-size slide

  36. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModele​
    >(


    val initialState: State,


    ) : ViewModel() {


    ...


    }​

    goo.gle/architecture-ui-events
    ViewEvent = User + UI logic events

    View full-size slide

  37. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModel
    <​
    ViewEvent, Stat
    e​
    >(


    val initialState: State,


    ) : ViewModel() {


    ...


    private val events = PublishSubject.create().toSerialized()


    ...


    }​

    View full-size slide

  38. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModel
    <​
    ViewEvent, Stat
    e​
    >(


    val initialState: State,


    ) : ViewModel() {


    ...


    private val events = PublishSubject.create().toSerialized()


    protected abstract fun onViewEvent(


    event: ViewEvent,


    )


    ...


    }​

    View full-size slide

  39. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    abstract class BaseTierViewModel
    <​
    ViewEvent, Stat
    e​
    >(


    val initialState: State,


    ) : ViewModel() {


    ...


    private val events = PublishSubject.create().toSerialized()


    protected abstract fun onViewEvent(


    event: ViewEvent,


    )


    fu
    n​
    triggerViewEvent(event: ViewEvent) {


    events.onNext(event)


    }


    }​

    View full-size slide

  40. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    val input = object : Input {


    override val articleId = PublishRelay.create()


    }​​​​



    android:onClick="@{ () -> vm.onBuyClicked() }”


    />
    💀

    View full-size slide

  41. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    val input = object : Input {


    override val articleId = PublishRelay.create()


    }




    android:onClick="@{ () -> vm.onBuyClicked() }”


    />
    sealed interface ArticleDetailsViewEvent {


    data class RenderArticle(


    val articleId: String


    ) : ArticleDetailsViewEvent


    object BuyClicked: ArticleDetailsViewEvent


    }

    View full-size slide

  42. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    val input = object : Input {


    override val articleId = PublishRelay.create()


    }​​​​



    android:onClick="@{ () -> vm.onBuyClicked() }”


    />
    sealed interface ArticleDetailsViewEvent {


    data class RenderArticle(


    val articleId: String


    ) : ArticleDetailsViewEvent


    object BuyClicked: ArticleDetailsViewEvent


    }

    View full-size slide

  43. @marcoGomier @stewemetal
    Untangling the spaghetti - View events
    val input = object : Input {


    override val articleId = PublishRelay.create()


    }​​​​



    android:onClick="@{ () -> vm.onBuyClicked() }”


    />
    sealed interface ArticleDetailsViewEvent {


    data class RenderArticle(


    val articleId: String


    ) : ArticleDetailsViewEvent


    object BuyClicked: ArticleDetailsViewEvent


    }

    View full-size slide

  44. @marcoGomier @stewemetal
    Untangling the spaghetti - ViewModel testing
    @Test


    fun `ArticleDetailsViewModel Test`()
    {​

    }​

    View full-size slide

  45. @marcoGomier @stewemetal
    Untangling the spaghetti - ViewModel testing
    @Test


    fun `ArticleDetailsViewModel Test`()
    {​

    val testObserver = viewModel.state().test()


    }​

    View full-size slide

  46. @marcoGomier @stewemetal
    Untangling the spaghetti - ViewModel testing
    @Test


    fun `ArticleDetailsViewModel Test`()
    {​

    val testObserver = viewModel.state().test()




    every { shopUseCase.getArticle(articleId)
    }​

    returns Single.just(defaultArticle)


    }​

    View full-size slide

  47. @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))


    }​

    View full-size slide

  48. @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 ->


    // …


    }​

    }​

    View full-size slide

  49. @marcoGomier @stewemetal
    ● Already established navigation


    ● Activities and Fragments


    ● Introducing new things one step at a time


    ● Migration hell ❌
    Untangling the spaghetti - Compose UI
    🫠

    View full-size slide

  50. @marcoGomier @stewemetal
    ● Already established navigation


    ● Activities and Fragments


    ● Introducing new things one step at a time


    ● Migration hell ❌
    Untangling the spaghetti - Compose UI
    🫠

    View full-size slide

  51. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    abstract class BaseComposeActivity : AppCompatActivity() {


    }

    View full-size slide

  52. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    abstract class BaseComposeActivity : AppCompatActivity() {


    abstra
    c​
    t v
    a​
    l viewModel: TierViewModel


    }

    View full-size slide

  53. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    abstract class BaseComposeActivity : AppCompatActivity() {


    abstra
    c​
    t v
    a​
    l viewModel: TierViewModel


    abstract val content: @Composable (State) -> Unit


    }

    View full-size slide

  54. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    abstract class BaseComposeActivity : AppCompatActivity() {


    abstra
    c​
    t v
    a​
    l viewModel: TierViewModel


    abstract val content: @Composable (State) -> Unit


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    }


    }

    View full-size slide

  55. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    abstract class BaseComposeActivity : AppCompatActivity() {


    abstra
    c​
    t v
    a​
    l viewModel: TierViewModel


    abstract val content: @Composable (State) -> Unit


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    }


    }


    }

    View full-size slide

  56. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    abstract class BaseComposeActivity : AppCompatActivity() {


    abstra
    c​
    t v
    a​
    l viewModel: TierViewModel


    abstract val content: @Composable (State) -> Unit


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    val state by viewModel.state()


    .subscribeAsState(initial = viewModel.initialState)


    }


    }


    }

    View full-size slide

  57. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    abstract class BaseComposeActivity : AppCompatActivity() {


    abstra
    c​
    t v
    a​
    l viewModel: TierViewModel


    abstract val content: @Composable (State) -> Unit


    override fun onCreate(savedInstanceState: Bundle?) {


    super.onCreate(savedInstanceState)


    setContent {


    val state by viewModel.state()


    .subscribeAsState(initial = viewModel.initialState)


    content(state)


    }


    }


    }

    View full-size slide

  58. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    internal class ArticleDetailsActivity :
    BaseComposeActivity() {


    ...




    }


    View full-size slide

  59. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    internal class ArticleDetailsActivity :
    BaseComposeActivity() {


    ...


    override val content = @Composable { state: ArticleDetailsState ->


    ArticleDetailsScreen(


    state = state,


    onBuyButtonClick =


    { viewModel.triggerViewEvent(BuyClicked) },


    )


    }


    }


    View full-size slide

  60. @marcoGomier @stewemetal
    Untangling the spaghetti - Compose UI
    internal class ArticleDetailsActivity :
    BaseComposeActivity() {


    ...


    override val content = @Composable { state: ArticleDetailsState ->


    ArticleDetailsScreen(


    state = state,


    onBuyButtonClick =


    { viewModel.triggerViewEvent(BuyClicked) },


    )


    }


    override fun onCreate(savedInstanceState: Bundle?) {


    ...


    viewModel.triggerViewEvent(RenderArticle(articleId))


    }


    }


    View full-size slide

  61. @marcoGomier @stewemetal
    Gradle
    Configuration 🐘🧙

    View full-size slide

  62. @marcoGomier @stewemetal
    Gradle Configuration
    ● Enable Compose only in some modules


    ● Enable and add Compose libraries with one line

    View full-size slide

  63. @marcoGomier @stewemetal
    Module Configuration → build.gradle
    plugins {


    id 'com.tier.app.android.library'


    id 'com.tier.app.android.library.compose'


    }

    View full-size slide

  64. @marcoGomier @stewemetal
    plugins {


    id 'com.tier.app.android.library'


    id 'com.tier.app.android.library.compose'


    }
    Module Configuration → build.gradle

    View full-size slide

  65. @marcoGomier @stewemetal
    class AndroidLibraryComposeConventionPlugin : Plugin {


    override fun apply(target: Project) {


    with(target) {


    pluginManager.apply("com.android.library")


    val extension = extensions.getByType()


    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

    View full-size slide

  66. @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

    View full-size slide

  67. @marcoGomier @stewemetal
    Design System

    View full-size slide

  68. @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

    View full-size slide

  69. @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
    OctopusButtonPrimary.kt


    R.layout.__internal_view_octopus_button


    R.styleable.OctopusButton


    R.drawable.__internal_octopus_button_background_primary


    @marcoGomier @stewemetal

    View full-size slide

  70. @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

    View full-size slide

  71. @marcoGomier @stewemetal
    Octopus Design System
    What about Compose?
    🤔

    View full-size slide

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

    View full-size slide

  73. @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

    View full-size slide

  74. @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

    View full-size slide

  75. @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 ->


    ...


    },


    )


    }


    View full-size slide

  76. @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

    View full-size slide

  77. @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

    View full-size slide

  78. @marcoGomier @stewemetal
    ● The benefits of pure Compose


    ● Easier to maintain


    ● No attachments to the legacy Views


    ● Easy for teams to adopt
    Octopus Design System
    🤩

    View full-size slide

  79. @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
    🫠

    View full-size slide

  80. @marcoGomier @stewemetal
    Octopus Design System - Going Compose-first
    ● Use the Compose implementation of components to render the
    Views


    ● Compose-View interop
    💡

    View full-size slide

  81. @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
    🔧

    View full-size slide

  82. @marcoGomier @stewemetal
    Octopus Design System - Going Compose-first

    View full-size slide

  83. @marcoGomier @stewemetal
    Octopus Design System - Going Compose-first

    View full-size slide

  84. @marcoGomier @stewemetal
    Octopus is Compose-first

    View full-size slide

  85. @marcoGomier @stewemetal
    In-house Compose


    onboarding 🏠

    View full-size slide

  86. @marcoGomier @stewemetal
    ● Architecture


    ● Octopus DS


    ● Screens


    Experiments

    View full-size slide

  87. @marcoGomier @stewemetal
    ● Architecture


    ● Octopus DS


    ● Screens
    Experiments

    View full-size slide

  88. @marcoGomier @stewemetal
    ● Regular 1-hour knowledge sharing sessions


    The Android Guild

    View full-size slide

  89. @marcoGomier @stewemetal
    ● Regular 1-hour knowledge sharing sessions


    ● A good place to start the onboarding
    The Android Guild

    View full-size slide

  90. @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/

    View full-size slide

  91. @marcoGomier @stewemetal
    The Android Guild
    🐙 Composing an Octopus

    View full-size slide

  92. @marcoGomier @stewemetal
    The Android Guild
    🐙 Composing an Octopus


    Compose Shorts

    View full-size slide

  93. @marcoGomier @stewemetal
    The Android Guild
    🐙 Composing an Octopus


    Compose Shorts

    View full-size slide

  94. @marcoGomier @stewemetal
    The Android Guild
    🐙 Composing an Octopus


    Compose Shorts

    View full-size slide

  95. @marcoGomier @stewemetal
    The Android Guild
    🐙 Composing an Octopus


    Compose Shorts

    View full-size slide

  96. @marcoGomier @stewemetal
    The Android Guild
    ● Materials in the main repo


    ● Sessions are recorded
    @marcoGomier @stewemetal

    View full-size slide

  97. @marcoGomier @stewemetal
    Conclusions

    View full-size slide

  98. @marcoGomier @stewemetal
    It dependsTM

    View full-size slide

  99. @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 😄

    View full-size slide

  100. 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/

    View full-size slide

  101. @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

    View full-size slide