$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

    View Slide

  2. @marcoGomier @stewemetal
    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
    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



    View Slide

  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


    }ㅤ

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


    }ㅤ

    }
    Legacy ViewModel
    Multiple Rx Relays
    ● Data Input


    ● Events, Navigation
    and (some) state
    @marcoGomier @stewemetal

    View Slide

  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


    }ㅤ

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


    }ㅤ

    }
    ● UI State split into multiple
    places


    ● Rx specific boilerplate all
    over the place
    @marcoGomier @stewemetal

    View Slide

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

    View Slide

  16. @marcoGomier @stewemetal
    Untangling the
    Spaghetti 🍝

    View Slide

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


    ● Make adoption as easy as possible


    ● Consider MAD Architecture suggestions

    View Slide

  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

    View Slide

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


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    }


    View Slide

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


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    }


    Untangling the spaghetti - UI state, UI events

    View Slide

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


    }


    val output = object : Output {


    override val articleNotFound = PublishRelay.create()


    override val state = BehaviorRelay.create()


    }


    }​

    Untangling the spaghetti - UI state, UI events

    View Slide

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


    ...


    ) : BaseViewModel()
    {​

    ...


    }​

    View Slide

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


    ...


    ) : BaseViewModel()
    {​

    ...


    }​

    https://goo.gle/architecture-state-holders

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


    }​

    View Slide

  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)


    ...


    }​

    View Slide

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


    ...


    }​

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    private val state = BehaviorSubject.createDefault(initialState)


    fun state(): Observable = state.hide()


    ...


    }

    sealed class ArticleDetailsUIState {


    object Loading: ArticleDetailsUIState()


    data

    class Content(


    val title: String,


    val price: String,


    val articleDetailsEvent: ArticleDetailsEvent? = null,


    ): ArticleDetailsUIState()


    }


    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    private val state = BehaviorSubject.createDefault(initialState)


    fun state(): Observable = state.hide()


    ...


    }

    data class ArticleDetailsUIState(


    val isLoading: Boolean = true,


    val title: String,


    val price: String,


    val articleDetailsEvent: ArticleDetailsEvent? = null,


    )


    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    private val state = BehaviorSubject.createDefault(initialState)


    fun state(): Observable = state.hide()


    ...


    }

    View Slide

  30. @marcoGomier @stewemetal
    Untangling the spaghetti - User events
    abstract class BaseTierViewModel(


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    View Slide

  31. @marcoGomier @stewemetal
    Untangling the spaghetti - User events
    abstract class BaseTierViewModel(


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    https:/goo.gle/architecture-ui-events

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


    }

    https:/goo.gle/architecture-ui-events

    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


    }


    View Slide

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


    val initialState: State,


    ) : ViewModel() {


    ...


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


    ...


    }​

    View Slide

  35. @marcoGomier @stewemetal
    Untangling the spaghetti - User 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

  36. @marcoGomier @stewemetal
    Untangling the spaghetti - User 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

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


    override val articleId = PublishRelay.create()


    }​​​​



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


    />
    💀

    View Slide

  38. @marcoGomier @stewemetal
    Untangling the spaghetti - User 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

  39. @marcoGomier @stewemetal
    Untangling the spaghetti - User 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

  40. @marcoGomier @stewemetal
    Untangling the spaghetti - User 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

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


    // …


    }​

    }​

    View Slide

  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

    View Slide

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


    // …


    }​

    }​

    View Slide

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


    // …


    }


    }​

    View Slide

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


    abstract val 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)


    }


    viewModel.onViewCreated()


    }


    }

    View Slide

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


    abstract val 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)


    }


    viewModel.onViewCreated()


    }


    }


    View Slide

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


    abstract val 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)


    }


    viewModel.onViewCreated()


    }


    }


    View Slide

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


    abstract val 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)


    }


    viewModel.onViewCreated()


    }


    }

    View Slide

  49. @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(val articleId: String))


    }


    View Slide

  50. @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(val articleId: String))


    }


    View Slide

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

  52. @marcoGomier @stewemetal
    Gradle
    Configuration

    View Slide

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


    ● Enable and add Compose libraries with one line

    View Slide

  54. @marcoGomier @stewemetal
    Module Configuration → buildSrc
    fun configureAndroidModule(


    project: Project,


    isDataBindingEnabled: Boolean,


    isComposeEnabled: Boolean = false,


    ) = project.libraryExtension.run {


    ...




    }

    View Slide

  55. @marcoGomier @stewemetal
    Module Configuration → buildSrc
    fun configureAndroidModule(


    project: Project,


    isDataBindingEnabled: Boolean,


    isComposeEnabled: Boolean = false,


    ) = project.libraryExtension.run {


    ...




    if (isComposeEnabled) {


    configureCompose(project)


    }


    }

    View Slide

  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)


    }


    }

    View Slide

  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

    View Slide

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


    id 'com.android.library'


    id 'kotlin-android'


    }


    ModuleConfig.configureAndroidModuleWithCompose(project)


    dependencies {


    ...


    }

    View Slide

  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 {


    ...


    }

    View Slide

  60. @marcoGomier @stewemetal
    Future: Use Compose BOM

    View Slide

  61. @marcoGomier @stewemetal
    Design System

    View Slide

  62. @marcoGomier @stewemetal
    Octopus Design System
    ● 100% Views with Data Binding and @BindingAdapters


    ● Good component APIs & docs


    ● Easy usage with View-based UI

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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


    ...


    },


    )


    }


    View Slide

  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

    View Slide

  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

    View Slide

  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
    🤩

    View Slide

  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
    🧐

    View Slide

  74. @marcoGomier @stewemetal
    Octopus Design System
    ● The future - going Compose-first


    ● Compose implementation behind the Views - ComposeView


    ● Life with minimal XML, and more Kotlin ❤

    View Slide

  75. @marcoGomier @stewemetal
    In-house Compose


    onboarding

    View Slide

  76. @marcoGomier @stewemetal
    ● Architecture


    ● Screens


    ● Octopus theme & components


    Experiments

    View Slide

  77. @marcoGomier @stewemetal
    ● Architecture


    ● Screens


    ● Octopus theme & components


    Experiments

    View Slide

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


    The Android Guild

    View Slide

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


    ● A good place to start the onboarding
    The Android Guild

    View Slide

  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/

    View Slide

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

    View Slide

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


    Compose Shorts

    View Slide

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


    Compose Shorts

    View Slide

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


    Compose Shorts

    View Slide

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


    Compose Shorts

    View Slide

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


    Compose Shorts

    View Slide

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


    ● Sessions are recorded
    @marcoGomier @stewemetal

    View Slide

  88. @marcoGomier @stewemetal
    Conclusions

    View Slide

  89. @marcoGomier @stewemetal
    It dependsTM

    View Slide

  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 😄

    View Slide

  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

    View Slide

  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

    View Slide