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

Adopting Jetpack Compose Safely (Droidcon Lisbon 2023)

István Juhos
September 29, 2023

Adopting Jetpack Compose Safely (Droidcon Lisbon 2023)

Presented as a joint talk with @marcoGomier at Droidcon Lisbon, 2023.09.29.

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

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

    View Slide

  2. Compose

    in the wild

    View Slide

  3. @marcoGomier @stewemetal
    WHY?

    View Slide

  4. @marcoGomier @stewemetal
    No more

    RecyclerView Adapters
    🥳

    View Slide

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


    😎 More fun


    😍 Kotlin

    View Slide

  6. @marcoGomier @stewemetal
    Start using Compose

    View Slide

  7. @marcoGomier @stewemetal
    🚧 ⛔ Start using Compose

    View Slide

  8. @marcoGomier @stewemetal
    Pre-Compose
    Architecture 🏗

    View 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 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 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 Slide

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

    View Slide

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

    View 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 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 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 Slide

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

    View Slide

  18. @marcoGomier @stewemetal
    Untangling the
    Spaghetti 🍝

    View Slide

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


    ● Make adoption as easy as possible


    ● Consider MAD Architecture suggestions

    View 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 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 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 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 Slide

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


    ...


    ) : BaseViewModel()
    {​

    ...


    }​

    View Slide

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


    ...


    ) : BaseViewModel()
    {​

    ...


    }​

    goo.gle/architecture-state-holders

    View Slide

  26. @marcoGomier @stewemetal
    Untangling the spaghetti - UI state, UI events
    abstract class BaseTierViewModel
    <​
    Stat
    e​
    >(


    v
    a​
    l initialState: State,


    ) : ViewModel() {


    ...


    }​

    View 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 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 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 ArticleDetailsUIStat
    e​
    (


    v​
    al isLoading: Boolean = true,


    v​
    al title: String,


    v​
    al price: String,


    v
    a​
    l articleDetailsEvent: ArticleDetailsEvent? = null,


    )


    View 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 ArticleDetailsUIStat
    e​
    (


    v​
    al isLoading: Boolean = true,


    v​
    al title: String,


    v​
    al price: String,


    v
    a​
    l articleDetailsEvent: ArticleDetailsEvent? = null,


    )


    View Slide

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


    ...


    }​

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    goo.gle/architecture-ui-events

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    goo.gle/architecture-ui-events

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


    }​

    goo.gle/architecture-ui-events

    View 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 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 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 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 Slide

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


    override val articleId = PublishRelay.create()


    }​​​​



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


    />
    💀

    View 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 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 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 Slide

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


    fun `ArticleDetailsViewModel Test`()
    {​

    }​

    View Slide

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


    fun `ArticleDetailsViewModel Test`()
    {​

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


    }​

    View 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 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 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 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 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 Slide

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


    }

    View Slide

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


    abstra
    c​
    t v
    a​
    l viewModel: TierViewModel


    }

    View 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 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 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 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 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 Slide

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


    ...




    }


    View 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 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 Slide

  61. @marcoGomier @stewemetal
    Gradle
    Configuration 🐘🧙

    View Slide

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


    ● Enable and add Compose libraries with one line

    View Slide

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


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


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


    }

    View Slide

  64. @marcoGomier @stewemetal
    plugins {


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


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


    }
    Module Configuration → build.gradle

    View 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 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 Slide

  67. @marcoGomier @stewemetal
    Design System

    View 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 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 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 Slide

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

    View Slide

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

    View 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 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 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 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 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 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 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 Slide

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


    ● Compose-View interop
    💡

    View 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 Slide

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

    View Slide

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

    View Slide

  84. @marcoGomier @stewemetal
    Octopus is Compose-first

    View Slide

  85. @marcoGomier @stewemetal
    In-house Compose


    onboarding 🏠

    View Slide

  86. @marcoGomier @stewemetal
    ● Architecture


    ● Octopus DS


    ● Screens


    Experiments

    View Slide

  87. @marcoGomier @stewemetal
    ● Architecture


    ● Octopus DS


    ● Screens
    Experiments

    View Slide

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


    The Android Guild

    View Slide

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


    ● A good place to start the onboarding
    The Android Guild

    View 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 Slide

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

    View Slide

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


    Compose Shorts

    View Slide

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


    Compose Shorts

    View Slide

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


    Compose Shorts

    View Slide

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


    Compose Shorts

    View Slide

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


    ● Sessions are recorded
    @marcoGomier @stewemetal

    View Slide

  97. @marcoGomier @stewemetal
    Conclusions

    View Slide

  98. @marcoGomier @stewemetal
    It dependsTM

    View 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 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 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 Slide