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

Composing a Design System

Composing a Design System

At TIER, we took the leap and adopted Jetpack Compose into our quite large code base. One of this journey's most significant milestones was having our design system, Octopus, implemented in pure Compose UI. In this talk, I present how we built Octopus with Compose, what we gained by reimplementing components instead of wrapping existing Views in AndroidView, and how we managed to support the still-View-based parts of our app with Compose.

Presented at the very first plDroid conference in Warsaw, Poland on 2023.05.30.

István Juhos

May 30, 2023
Tweet

More Decks by István Juhos

Other Decks in Programming

Transcript

  1. @stewemetal
    Composing a


    Design System
    👨💻 Senior Android Engineer @ TIER


    Co-organizer of Kotlin Budapest

    🌐 istvanjuhos.dev
    István Juhos

    View Slide

  2. @stewemetal
    @akoskovacsme

    View Slide

  3. @stewemetal
    @akoskovacsme

    View Slide

  4. @stewemetal
    Octopus Design System - B.C.
    ● Customised Android Views and ViewGroups


    ● Data Binding and @BindingAdapters


    ● Lots of XML files and custom XML attributes

    View Slide

  5. @stewemetal
    Octopus Design System - B.C.
    ● Customised Android Views and ViewGroups


    ● Data Binding and @BindingAdapters


    ● Lots of XML files and custom XML attributes
    OctopusButtonPrimary.kt


    R.layout.__internal_view_octopus_button


    R.styleable.OctopusButton


    R.drawable.__internal_octopus_button_background_primary


    ...


    @stewemetal

    View Slide

  6. @stewemetal
    Octopus Design System - B.C.
    ● Customised Android Views and ViewGroups


    ● Data Binding and @BindingAdapters


    ● Lots of XML files and custom XML attributes
    class OctopusText @JvmOverloads constructor(


    context: Context,


    attrs: AttributeSet? = null,


    defStyleAttr: Int = 0,


    ) : AppCompatTextView(context, attrs, defStyleAttr)


    @stewemetal

    View Slide

  7. @stewemetal
    Octopus Design System - B.C.
    ● Customised Android Views and ViewGroups


    ● Data Binding and @BindingAdapters


    ● Lots of XML files and custom XML attributes
    class OctopusText @JvmOverloads constructor(


    context: Context,


    attrs: AttributeSet? = null,


    defStyleAttr: Int = 0,


    ) : AppCompatTextView(context, attrs, defStyleAttr)


    @stewemetal

    View Slide

  8. @stewemetal
    Octopus Design System - B.C.
    private fun updateStyle() {


    areSettersEnabled = true


    ...

    setTextColor(ContextCompat.getColor(context, colorFromTextType))


    ...

    areSettersEnabled = false


    }


    OctopusText : AppCompatTextView


    // We have to disable specific setters to prevent misuse of the view


    private var areSettersEnabled = false


    View Slide

  9. @stewemetal
    Octopus Design System - B.C.
    /**


    * Calling this method will have no effect.


    */


    override fun setTextColor(color: Int) {


    if (areSettersEnabled || isInEditMode) {


    super.setTextColor(color)


    }


    }
    OctopusText : AppCompatTextView


    // We have to disable specific setters to prevent misuse of the view


    private var areSettersEnabled = false


    View Slide

  10. @stewemetal
    Adopting Compose

    🤔

    View Slide

  11. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    @marcoGomier @stewemetal

    View Slide

  12. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    Views + AndroidView
    @Composable


    fun OctopusButtonPrimary(


    text: String,


    ...


    onClick: () -> Unit,


    ) {


    AndroidView(


    factory = { context ->


    OctopusButtonPrimary(


    ContextThemeWrapper(


    context,


    R.style.Theme_Octopus,


    )


    )


    },


    update = { view ->


    ...


    },


    )


    }


    @stewemetal

    View Slide

  13. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    Views + AndroidView
    @Composable


    fun OctopusButtonPrimary(


    text: String,


    ...


    onClick: () -> Unit,


    ) {


    AndroidView(


    factory = { context ->


    OctopusButtonPrimary(


    ContextThemeWrapper(


    context,


    R.style.Theme_Octopus,


    )


    )


    },


    update = { view ->


    ...


    },


    )


    }


    @stewemetal

    View Slide

  14. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    Views + AndroidView
    @Composable


    fun OctopusButtonPrimary(


    text: String,


    ...


    onClick: () -> Unit,


    ) {


    AndroidView(


    factory = { context ->


    OctopusButtonPrimary(


    ContextThemeWrapper(


    context,


    R.style.Theme_Octopus,


    )


    ).apply {


    setText(text)


    setOnClickListener {


    onClick()


    }


    ...


    }


    },


    update = { view ->




    @stewemetal

    View Slide

  15. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    Views + AndroidView + XMLs
    @Composable


    fun OctopusButtonPrimary(


    text: String,


    ...


    onClick: () -> Unit,


    ) {


    AndroidView(


    factory = { context ->


    OctopusButtonPrimary(


    ContextThemeWrapper(


    context,


    R.style.Theme_Octopus,


    )


    ).apply {


    setText(text)


    setOnClickListener {


    onClick()


    }


    ...


    }


    },


    update = { view ->




    @stewemetal

    View Slide

  16. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    @Composable


    fun OctopusButtonPrimary(


    text: String,


    ...


    onClick: () -> Unit,


    ) {


    AndroidView(


    factory = { context ->


    OctopusButtonPrimary(


    ContextThemeWrapper(


    context,


    R.style.Theme_Octopus,


    )


    ).apply {


    setText(text)


    setOnClickListener {


    onClick()


    }


    ...


    }


    },


    update = { view ->




    @stewemetal
    Views + AndroidView + XMLs

    View Slide

  17. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    @Composable


    public fun OctopusButtonPrimary(


    text: String,


    modifier: Modifier = Modifier,


    buttonSize: ButtonSize = NORMAL,


    enabled: Boolean = true,


    loading: Boolean = false,


    destructive: Boolean = false,


    onClick: () -> Unit,


    ) {


    Box() {


    Button(…) {


    Text(text)


    }


    if (loading) {


    OctopusButtonLoader()


    }


    }


    }


    Reimplement component
    s​

    in

    Compose
    @stewemetal

    View Slide

  18. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    @Composable


    public fun OctopusButtonPrimary(


    text: String,


    modifier: Modifier = Modifier,


    buttonSize: ButtonSize = NORMAL,


    enabled: Boolean = true,


    loading: Boolean = false,


    destructive: Boolean = false,


    onClick: () -> Unit,


    ) {


    Box() {


    Button(…) {


    Text(text)


    }


    if (loading) {


    OctopusButtonLoader()


    }


    }


    }


    Reimplement component
    s​

    in

    Compose
    @stewemetal

    View Slide

  19. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    Reimplement component
    s​

    in

    Compose
    @stewemetal
    @Composable


    public fun OctopusButtonPrimary(


    text: String,


    modifier: Modifier = Modifier,


    buttonSize: ButtonSize = NORMAL,


    enabled: Boolean = true,


    loading: Boolean = false,


    destructive: Boolean = false,


    onClick: () -> Unit,


    ) {


    Box() {


    Button(…) {


    Text(text)


    }


    if (loading) {


    OctopusButtonLoader()


    }


    }


    }


    View Slide

  20. @stewemetal
    Adopting Compose
    ● Possible approaches to consider
    Reimplement component
    s​

    in

    Compose
    @stewemetal
    @Composable


    public fun OctopusButtonPrimary(


    text: String,


    modifier: Modifier = Modifier,


    buttonSize: ButtonSize = NORMAL,


    enabled: Boolean = true,


    loading: Boolean = false,


    destructive: Boolean = false,


    onClick: () -> Unit,


    ) {


    Box() {


    Button(…) {


    Text(text)


    }


    if (loading) {


    OctopusButtonLoader()


    }


    }


    }


    View Slide

  21. @stewemetal
    ● The benefits of recreating components in Compose


    ● Easier to maintain than custom Views


    ● No attachments to the legacy Views and tech debt


    ● Octopus is close to Material


    ● A new experience for our teams
    Adopting Compose
    🤩

    View Slide

  22. @stewemetal
    ● Downsides of recreating components in Compose


    ● Lack of experience in Compose APIs


    ● A second implementation to maintain


    ● Parity with the View-based components
    Adopting Compose
    🧐

    View Slide

  23. @stewemetal
    Adopting Compose

    View Slide

  24. @stewemetal
    Composing Octopus
    developer.android.com/jetpack/compose/designsystems

    View Slide

  25. @stewemetal
    Composing Octopus
    ● Three possible approaches (not just for the theme!)


    ● Customize Material


    ● Extend Material


    ● Go fully-custom

    View Slide

  26. @stewemetal
    Composing Octopus
    ● Three possible approaches (not just for the theme!)


    ● Customize Material


    ● Extend Material


    ● Go fully-custom

    View Slide

  27. @stewemetal
    Composing Octopus
    ● Three possible approaches (not just for the theme!)


    ● Customize Material


    ● Extend Material


    ● Go fully-custom - but use Material components where possible

    View Slide

  28. @stewemetal
    OctopusTheme

    View Slide

  29. @stewemetal
    OctopusTheme
    @Composable


    public fun OctopusTheme(


    isInDarkMode: Boolean = isSystemInDarkTheme(),

    ...


    content: @Composable () -> Unit,


    ) {


    content()


    }


    View Slide

  30. @stewemetal
    @Composable


    public fun OctopusTheme(


    isInDarkMode: Boolean = isSystemInDarkTheme(),


    colors: OctopusColors = OctopusColors.build(isInDarkMode),


    typography: OctopusTypography = OctopusTypography.build(),


    shapes: OctopusShapes = OctopusShapes.build(),


    dimensions: OctopusDimensions = OctopusDimensions.build(),


    spacings: OctopusSpacings = OctopusSpacings(),


    content: @Composable () -> Unit,


    ) {


    content()


    }


    OctopusTheme

    View Slide

  31. @stewemetal
    @Composable


    public fun OctopusTheme(


    isInDarkMode: Boolean = isSystemInDarkTheme(),


    colors: OctopusColors = OctopusColors.build(isInDarkMode),


    ...


    content: @Composable () -> Unit,


    ) {


    CompositionLocalProvider(


    LocalColors provides colors,


    ...


    ) {


    content()


    }


    }


    OctopusTheme

    View Slide

  32. @stewemetal
    @Composable


    public fun OctopusTheme(


    isInDarkMode: Boolean = isSystemInDarkTheme(),


    colors: OctopusColors = OctopusColors.build(isInDarkMode),


    ...


    content: @Composable () -> Unit,


    ) {


    CompositionLocalProvider(


    LocalColors provides colors,


    ...


    ) {


    content()


    }


    }


    OctopusTheme

    View Slide

  33. @stewemetal
    private val LocalColors = compositionLocalOf {


    error(

    "No colors provided! Make sure to wrap all Octopus components

    in an OctopusTheme."

    )


    }


    OctopusTheme

    View Slide

  34. @stewemetal
    public object {


    public val : OctopusColors


    @Composable


    @ReadOnlyComposable


    get() = LocalColors.current


    ...


    }
    OctopusTheme
    colors
    OctopusTheme

    View Slide

  35. @stewemetal
    Modifier


    .background(


    color = . .semanticColors.backgroundPrimary,


    )


    OctopusTheme
    colors
    OctopusTheme

    View Slide

  36. @stewemetal
    OctopusTheme
    developer.android.com/jetpack/compose/compositionlocal

    View Slide

  37. @stewemetal
    Composable
    Components

    View Slide

  38. @stewemetal
    Designing Components in Compose
    goo.gle/compose-api-guidelines

    View Slide

  39. @stewemetal
    ● Octopus Compose component implementations


    ● should follow the Jetpack Compose library API guidelines


    ● should look and feel like the View ones (DS specs ✅ )


    ● should use values provided by OctopusTheme


    ● should be built on existing Material components, where viable
    Designing Components in Compose

    View Slide

  40. @stewemetal
    Component example - OctopusText
    @Composable


    public fun OctopusText(


    text: String,


    modifier: Modifier = Modifier,


    textType: TextType = BODY_1,


    links: List = emptyList(),


    textAlignment: Alignment = NATURAL,


    overflow: TextOverflow = TextOverflow.Clip,


    style: OctopusStyle = OctopusStyle.getDefault(),


    ) {


    ...


    }

    View Slide

  41. @stewemetal
    Component example - OctopusText
    @Composable


    public fun OctopusText(


    text: String,


    modifier: Modifier = Modifier,


    textType: TextType = BODY_1,


    links: List = emptyList(),


    textAlignment: Alignment = NATURAL,


    overflow: TextOverflow = TextOverflow.Clip,


    style: OctopusStyle = OctopusStyle.getDefault(),


    ) {


    ...


    }

    View Slide

  42. @stewemetal
    Component example - OctopusText
    @Composable


    public fun OctopusText(


    text: String,


    modifier: Modifier = Modifier,


    textType: TextType = BODY_1,


    links: List = emptyList(),


    textAlignment: Alignment = NATURAL,


    overflow: TextOverflow = TextOverflow.Clip,


    style: OctopusStyle = OctopusStyle.getDefault(),


    ) {


    ...


    }

    View Slide

  43. @stewemetal
    Configuration example - TextType
    enum class TextType(


    private val id: Int,


    @ColorRes public val textColorRes: Int,


    public val textSizeSp: Float,


    public val lineHeightSp: Float,


    public val isBold: Boolean = true,


    public val maxLines: Int = Int.MAX_VALUE,


    public val isAllCaps: Boolean = false,


    ) {


    TITLE_1(0, R.color.__internal_octopusTextTitleNormal, SIZE_TITLE_1,

    LINE_HEIGHT_40, maxLines = 1),


    BODY_1(1, R.color.__internal_octopusTextBody, TEXT_SIZE_16,

    LINE_HEIGHT_20, isBold = false),


    ...


    }

    View Slide

  44. @stewemetal
    TextType usage with Composables
    OctopusText(


    text = "Title 1 (Normal)",


    textType = TITLE_1,


    )


    OctopusText(


    text = "Title 1 (Informative)",


    textType = TITLE_1_INFORMATIVE,


    )


    OctopusText(


    text = "Title 1 (Destructive)",


    textType = TITLE_1_DESTRUCTIVE,


    )


    ...


    View Slide

  45. @stewemetal


    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    android:text="Title 1 (Normal)"


    app:textType="title1"


    />




    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    android:text="Title 1 (Informative)"


    app:textType=“title1Informative"


    />


    TextType usage with Views

    View Slide

  46. @stewemetal


    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    android:text="Title 1 (Normal)"


    app:textType="title1"


    />




    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    android:text="Title 1 (Informative)"


    app:textType=“title1Informative"


    />


    TextType usage with Views

    View Slide

  47. @stewemetal






























    ...





    ...





    😵💫
    TextType usage with Views

    View Slide

  48. @stewemetal
    Previews 🤩

    View Slide

  49. @stewemetal
    Previews
    ● At least light and dark variants


    ● Multi-preview
    🐬

    View Slide

  50. @stewemetal
    Previews
    ● At least light and dark variants


    ● Multi-preview

    View Slide

  51. @stewemetal
    Previews
    @Preview(


    name = "1 - Light",


    showBackground = true,


    backgroundColor = 0xffffffff, // primaryBackgroundLight


    group = "Light/Dark",


    )


    @Preview(


    name = "2 - Dark",


    showBackground = true,


    uiMode = UI_MODE_NIGHT_YES,


    backgroundColor = 0xff060a1e, // primaryBackgroundDark


    group = "Light/Dark",


    )


    annotation class OctopusPreviewPrimaryBackground


    View Slide

  52. @stewemetal
    Previews
    @OctopusPreviewPrimaryBackground


    @Composable


    fun OctopusTextPreview() {


    Column(


    modifier = Modifier.verticalScroll(rememberScrollState()),


    ) {


    OctopusTextComposeDemo()


    }


    }


    View Slide

  53. @stewemetal
    Previews
    @OctopusPreviewPrimaryBackground


    @Composable


    fun OctopusTextPreview() {


    Column(


    modifier = Modifier.verticalScroll(rememberScrollState()),


    ) {


    OctopusTextComposeDemo()


    }


    }


    View Slide

  54. @stewemetal
    Previews
    @OctopusPreviewPrimaryBackground


    @Composable


    fun OctopusTextPreview() {


    Column(


    modifier = Modifier.verticalScroll(rememberScrollState()),


    ) {


    OctopusTextComposeDemo()


    }


    }


    View Slide

  55. @stewemetal
    Previews
    @OctopusPreviewPrimaryBackground


    @Composable


    fun OctopusTextPreview() {


    Column(


    modifier = Modifier.verticalScroll(rememberScrollState()),


    ) {


    OctopusTextComposeDemo()


    }


    }


    View Slide

  56. @stewemetal
    Previews
    @OctopusPreviewPrimaryBackground


    @Composable


    fun OctopusTextPreview() {


    Column(


    modifier = Modifier.verticalScroll(rememberScrollState()),


    ) {


    OctopusTextComposeDemo()


    }


    }


    View Slide

  57. @stewemetal
    Previews in the in-app Octopus demo
    @Composable


    fun OctopusTextComposeDemo() {


    OctopusTheme {


    Column(...) {


    OctopusComposeDemoSubSection(


    title = “Titles",


    ) {


    OctopusText(


    text = "Title 1 (Normal)",


    textType = TITLE_1,


    )


    OctopusText(


    text = "Title 1 (Informative)",


    textType = TITLE_1_INFORMATIVE,


    )


    OctopusText(



    View Slide

  58. @stewemetal
    Previews in the in-app Octopus demo
    @Composable


    fun OctopusTextComposeDemo() {


    OctopusTheme {


    Column(...) {


    OctopusComposeDemoSubSection(


    title = “Titles",


    ) {


    OctopusText(


    text = "Title 1 (Normal)",


    textType = TITLE_1,


    )


    OctopusText(


    text = "Title 1 (Informative)",


    textType = TITLE_1_INFORMATIVE,


    )


    OctopusText(



    View Slide

  59. @stewemetal
    Previews in the in-app Octopus demo



    ...




    android:text="Title 1 (Normal)"


    app:textType="title1" />


    ...





    View Slide

  60. @stewemetal
    Previews in the in-app Octopus demo



    ...




    android:text="Title 1 (Normal)"


    app:textType="title1" />


    ...







    android:id="@+id/composeView"


    android:layout_width="match_parent"


    android:layout_height="wrap_content" />

    View Slide

  61. @stewemetal
    Previews in the in-app Octopus demo



    ...




    android:text="Title 1 (Normal)"


    app:textType="title1" />


    ...




    android:id="@+id/composeView"


    android:layout_width="match_parent"


    android:layout_height="wrap_content" />





    ...

    View Slide

  62. @stewemetal
    Previews in the in-app Octopus demo
    findViewById(R.id.composeView)?.apply {


    setContent {


    OctopusTheme {


    OctopusComposeDemoSection {


    OctopusTextComposeDemo()


    }


    }


    }


    }


    ...

    View Slide

  63. @stewemetal
    Previews in the in-app Octopus demo
    findViewById(R.id.composeView)?.apply {


    setContent {


    OctopusTheme {


    OctopusComposeDemoSection {


    OctopusTextComposeDemo()


    }


    }


    }


    }


    ...

    View Slide

  64. @stewemetal
    In-app Octopus Demo

    View Slide

  65. @stewemetal
    In-app Octopus Demo

    View Slide

  66. @stewemetal
    Going
    Compose-first

    View Slide

  67. @stewemetal
    Going Compose-first
    ● Two separate Design System implementations 👎


    ● Duplicated presentation and behavior


    ● Keeping them in sync is tedious


    ● New components = twice the work
    🫠

    View Slide

  68. @stewemetal
    Going Compose-first
    💡

    View Slide

  69. @stewemetal
    Going Compose-first
    ● Use the Compose implementation of components to render the
    Views
    💡

    View Slide

  70. @stewemetal
    Going Compose-first
    ● Use the Compose implementation of components to render the
    Views


    ● Compose-View interop
    💡

    View Slide

  71. @stewemetal
    Going Compose-first
    ● Compose-View interop
    @stewemetal


    android:id="@+id/someView"


    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    />

    💡

    View Slide

  72. @stewemetal
    Going Compose-first
    ● Compose-View interop
    @stewemetal


    android:id="@+id/someView"


    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    />

    💡 findViewById(R.id.someView)


    .setContent {


    OctopusTheme {


    ...

    }


    }


    View Slide

  73. @stewemetal
    Going Compose-first
    @stewemetal
    class ComposeView @JvmOverloads constructor(


    context: Context,


    attrs: AttributeSet? = null,


    defStyleAttr: Int = 0


    ) : AbstractComposeView(
    ...
    ) {


    private val content =


    mutableStateOf<(@Composable ()
    ->
    Unit)?>(null)


    @Composable


    override fun Content() {


    content.value
    ?.
    invoke()


    }


    }


    View Slide

  74. @stewemetal
    Going Compose-first
    @stewemetal
    class ComposeView @JvmOverloads constructor(


    context: Context,


    attrs: AttributeSet? = null,


    defStyleAttr: Int = 0


    ) : AbstractComposeView(
    ...
    ) {


    private val content =


    mutableStateOf<(@Composable ()
    ->
    Unit)?>(null)


    @Composable


    override fun Content() {


    content.value
    ?.
    invoke()


    }


    }


    View Slide

  75. @stewemetal
    Going Compose-first
    @stewemetal
    class ComposeView @JvmOverloads constructor(


    context: Context,


    attrs: AttributeSet? = null,


    defStyleAttr: Int = 0


    ) : AbstractComposeView(
    ...
    ) {


    private val content =


    mutableStateOf<(@Composable ()
    ->
    Unit)?>(null)


    @Composable


    override fun Content() {


    content.value
    ?.
    invoke()


    }


    }


    🤔

    View Slide

  76. @stewemetal
    Going Compose-first
    @stewemetal
    class ComposeView @JvmOverloads constructor(


    context: Context,


    attrs: AttributeSet? = null,


    defStyleAttr: Int = 0


    ) : AbstractComposeView(
    ...
    ) {


    private val content =


    mutableStateOf<(@Composable ()
    ->
    Unit)?>(null)


    @Composable


    override fun Content() {


    content.value
    ?.
    invoke()


    }


    }


    🤔

    View Slide

  77. @stewemetal
    Going Compose-first
    ● AbstractComposeView

    View Slide

  78. @stewemetal
    View wrappers - base class
    abstract class OctopusComposeBaseView @JvmOverloads constructor(


    ...


    ) : AbstractComposeView(...) {


    ...


    }


    View Slide

  79. @stewemetal
    View wrappers - base class
    abstract class OctopusComposeBaseView @JvmOverloads constructor(


    ...


    ) : AbstractComposeView(...) {


    @get:StyleableRes


    protected open val styleables: IntArray = IntArray(0)




    ...


    }


    View Slide

  80. @stewemetal
    View wrappers - base class
    abstract class OctopusComposeBaseView() : AbstractComposeView(...) {


    @get:StyleableRes


    protected open val styleables: IntArray = IntArray(0)


    protected fun initView(attrs: AttributeSet?) {


    if (styleables.isNotEmpty()) {


    context.theme

    .obtainStyledAttributes(attrs, styleables, 0, 0,)


    .apply {


    try { initAttributes(this) }


    finally { recycle() }


    }


    }


    }


    }


    View Slide

  81. @stewemetal
    View wrappers - base class
    abstract class OctopusComposeBaseView() : AbstractComposeView(...) {


    @get:StyleableRes


    protected open val styleables: IntArray = IntArray(0)


    protected fun initView(attrs: AttributeSet?) { ... }


    protected abstract fun initAttributes(typedArray: TypedArray)


    }


    View Slide

  82. @stewemetal
    Supporting custom properties


    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    app:text="Primary button" />


    View Slide

  83. @stewemetal
    Supporting custom properties


    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    app:enabled="false"


    app:text="Primary button" />


    View Slide

  84. @stewemetal
    Supporting custom properties


    android:layout_width="match_parent"


    android:layout_height="wrap_content"


    app:destructive="true"


    app:text="Primary button" />


    View Slide

  85. @stewemetal
    Supporting custom properties
    class OctopusButtonPrimary(...) : OctopusComposeBaseView(...) {


    @StyleableRes


    override val styleables: IntArray = R.styleable.OctopusButton


    init { initView(attrs) }


    override fun initAttributes(typedArray: TypedArray) {


    with(typedArray) {


    text.value = getString(R.styleable.OctopusButton_text).orEmpty()


    isEnabled.value = getBoolean(R.styleable.OctopusButton_enabled, true)


    isDestructive.value = getBoolean(R.styleable.OctopusButton_destructive,
    false)


    }


    }


    }


    View Slide

  86. @stewemetal
    Supporting custom properties
    class OctopusButtonPrimary(...) : OctopusComposeBaseView(...) {


    @StyleableRes


    override val styleables: IntArray = R.styleable.OctopusButton


    init { initView(attrs) }


    override fun initAttributes(typedArray: TypedArray) {


    with(typedArray) {


    text.value = getString(R.styleable.OctopusButton_text).orEmpty()


    isEnabled.value = getBoolean(R.styleable.OctopusButton_enabled, true)


    isDestructive.value = getBoolean(R.styleable.OctopusButton_destructive,
    false)


    }


    }


    }


    View Slide

  87. @stewemetal
    Supporting custom properties
    class OctopusButtonPrimary(...) : OctopusComposeBaseView(...) {


    @StyleableRes


    override val styleables: IntArray = R.styleable.OctopusButton


    init { initView(attrs) }


    override fun initAttributes(typedArray: TypedArray) {


    with(typedArray) {


    text.value = getString(R.styleable.OctopusButton_text).orEmpty()


    isEnabled.value = getBoolean(R.styleable.OctopusButton_enabled, true)


    isDestructive.value = getBoolean(R.styleable.OctopusButton_destructive,
    false)


    }


    }


    }


    View Slide

  88. @stewemetal
    Supporting custom properties
    class OctopusButtonPrimary(...) : OctopusComposeBaseView(...) {


    @StyleableRes


    override val styleables: IntArray = R.styleable.OctopusButton


    init { initView(attrs) }


    override fun initAttributes(typedArray: TypedArray) {


    with(typedArray) {


    text.value = getString(R.styleable.OctopusButton_text).orEmpty()


    isEnabled.value = getBoolean(R.styleable.OctopusButton_enabled, true)


    isDestructive.value = getBoolean(R.styleable.OctopusButton_destructive,
    false)


    }


    }


    }


    View Slide

  89. @stewemetal
    Supporting custom properties
    class OctopusButtonPrimary(...) : OctopusComposeBaseView(...) {


    ...


    private val text = mutableStateOf("")


    private val isEnabled = mutableStateOf(true)


    private val isDestructive = mutableStateOf(false)




    override fun initAttributes(typedArray: TypedArray) {


    with(typedArray) {


    text.value = getString(R.styleable.OctopusButton_text).orEmpty()


    isEnabled.value = getBoolean(R.styleable.OctopusButton_enabled, true)


    isDestructive.value = getBoolean(R.styleable.OctopusButton_destructive,
    false)


    }


    }


    }


    View Slide

  90. @stewemetal
    Supporting custom properties
    class OctopusButtonPrimary(...) : OctopusComposeBaseView(...) {


    ...


    @Composable


    override fun Content() {


    OctopusTheme {


    OctopusButtonPrimaryCompose(


    text = text.value,


    enabled = isEnabled.value,


    destructive = isDestructive.value,


    )


    }


    }


    }


    View Slide

  91. @stewemetal
    Supporting custom properties
    class OctopusButtonPrimary(...) : OctopusComposeBaseView(...) {


    ...


    @Composable


    override fun Content() {


    OctopusTheme {


    OctopusButtonPrimaryCompose(


    text = text.value,


    enabled = isEnabled.value,


    destructive = isDestructive.value,


    )


    }


    }


    }


    import
    com.tier.octopus.compose.widget
    .button.OctopusButtonPrimary


    as OctopusButtonPrimaryCompose


    View Slide

  92. @stewemetal
    XML previews


    ...


    app:text="Primary button" />




    ...


    app:enabled="false"


    app:text="Primary button" />




    ...


    app:destructive="true"


    app:text="Primary button" />



    View Slide

  93. @stewemetal
    XML previews


    ...


    app:text="Primary button" />




    ...


    app:enabled="false"


    app:text="Primary button" />




    ...


    app:destructive="true"


    app:text="Primary button" />


    ⚠ Works out of the box after Android Studio Giraffe Canary 8
    https://issuetracker.google.com/issues/187339385

    View Slide

  94. @stewemetal
    Wrapping up

    View Slide

  95. @stewemetal
    Octopus is Compose-first

    View Slide

  96. @stewemetal

    View Slide

  97. @stewemetal
    Was it worth it?

    View Slide

  98. @stewemetal
    It dependsTM

    View Slide

  99. @stewemetal
    Conclusions
    ● Having a design system to convert to Compose is already a good
    position

    View Slide

  100. @stewemetal
    Conclusions
    ● Having a design system to convert to Compose is already a good
    position


    ● Choose a path that makes sense for your project and timeline

    View Slide

  101. @stewemetal
    Conclusions
    ● Having a design system to convert to Compose is already a good
    position


    ● Choose a path that makes sense for your project and timeline


    ● Consider interoperability with legacy Views

    View Slide

  102. @stewemetal
    Conclusions
    ● Having a design system to convert to Compose is already a good
    position


    ● Choose a path that makes sense for your project and timeline


    ● Consider interoperability with legacy Views


    ● Going Compose-first is a challenging journey

    View Slide

  103. @stewemetal
    Conclusions
    ● Having a design system to convert to Compose is already a good
    position


    ● Choose a path that makes sense for your project and timeline


    ● Consider interoperability with legacy Views


    ● Going Compose-first is a challenging journey


    ● Zero ➡ Hero takes time
    We’re still here 😄

    View Slide

  104. @stewemetal
    ● https://developer.android.com/jetpack/compose/designsystems


    ● https://adambennett.dev/2020/12/migrating-your-design-system-to-jetpack-
    compose-part-1/


    ● https://www.droidcon.com/2022/06/28/custom-design-systems-in-compose/


    ● https://github.com/androidx/androidx/blob/androidx-main/compose/docs/
    compose-api-guidelines.md


    ● https://developer.android.com/jetpack/compose/compositionlocal


    ● https://developer.android.com/reference/kotlin/androidx/compose/ui/
    platform/AbstractComposeView
    Resources

    View Slide

  105. @stewemetal
    Composing a Design System
    István Juhos
    👨💻 Senior Android Engineer @ TIER


    Co-organizer of Kotlin Budapest

    Mastodon: androiddev.social/@stewemetal

    Twitter: @stewemetal

    Web: istvanjuhos.dev

    Thank you! / Dziękuję!


    View Slide