automotive-design-compose icon indicating copy to clipboard operation
automotive-design-compose copied to clipboard

DesignVariant not working with figma component instances

Open miguel-galarza opened this issue 1 year ago • 11 comments

With the purpose of reusing Figma components, it is usually created a generic component in Figma with different variants. For example a #buttonGeneric with two variants (Enabled, Disabled) associated to a property #buttonState. From Figma, then different instances of the #buttonGeneric could be added in a screen and for each one it is possible to select a specific variant by modifying the property #buttonState. The image below, describes the use case:

The problem is that this approach of implementation is not working properly in design compose. When @DesignVariant is used, it is not possible to associate each specific instance to different buttons in the screen (nodes). Moreover when the variant is applied, the parameters of the node are lost:

  • Tap on button stop working (TapCallback not triggered)
  • TextButton associated references to the text of the #buttonGeneric instead of the associated to the node
  • Icons associated references to the icon of the #buttonGeneric instead of the associated to the node

It seems that when using DesignVariant a complete replacement of the instance by the generic one occurs.

The following issue Cannot change variant of a component on the stage · Issue #691 · google/automotive-design-compose (github.com) reports a similar perspective of the issue but this workaround add additional complexity to the code and does not solve the TapCallback issue. The approach also loses the simplicity of adding simple nodes.

It would be really helpful to apply specific variants to specific nodes without the need of creating additional components for each case.

miguel-galarza avatar Jul 08 '24 08:07 miguel-galarza

Thank you for the detailed request. I've assigned this to @iamralpht so that we can triage this soon.

timothyfroehlich avatar Jul 11 '24 14:07 timothyfroehlich

Are you trying to change the button variant at runtime? If not, you don't need to use @DesignVariant. Then you can name the instances differently and use those node names in your @Design annotations.

rylin8 avatar Jul 17 '24 21:07 rylin8

Hi @rylin8, Yes, it is needed to change the button style at runtime, and there may be different states for different buttons in the same screen.

miguel-galarza avatar Jul 19 '24 06:07 miguel-galarza

One thing you could do is create placeholder nodes in your main frame with different names like #button1, #button2 etc. Then put @Composable (ComponentReplacementContext) -> Unit customizations on those nodes and replace them with a @Composable generated from a @DesignComponent function for that button. For an example see https://github.com/google/automotive-design-compose/blob/05569c2d087855e5be7b883be330bbe9c92144be/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariantPropertiesTest.kt#L101

rylin8 avatar Jul 19 '24 17:07 rylin8

Hi @rylin8,

I have tested the proposed solution by also adding a text and a TapCallback to the replacement composable. In the example provided in VariantPropertiesTest.kt, it would be something like this:

@DesignComponent(node = "#SquareBorder") fun Square( @DesignVariant(property = "BorderType") type: SquareBorder, @DesignVariant(property = "#SquareColor") color: SquareColor, @DesignVariant(property = "#SquareShadow") shadow: Shadow, @Design(node = "#SquareBorder") onTapCallback: TapCallback, @Design(node = "#text") textButton: String )

Independent buttons are now working as expected taking the states "Enabled" and "Disabled", but if we add some prototype change from Figma itself, for example a "While Pressing" then if two buttons have the same state both will be changed during the "While pressing". ButtonStates

Is there some solution for this?

Thank you and BR,

miguel-galarza avatar Jul 23 '24 10:07 miguel-galarza

I'm not sure why that wouldn't be working. We have a test that does something very similar to what you're describing. Please check this example and see if it is similar to your scenario: https://github.com/google/automotive-design-compose/blob/main/integration-tests/validation/src/main/java/com/android/designcompose/testapp/validation/examples/VariantInteractionsTest.kt

rylin8 avatar Jul 23 '24 16:07 rylin8

@miguel-galarza Can you share your example (or a reduction of your example) that reproduces this issue, because we think we have a test case that covers it (but must have a variation that we're not covering)?

iamralpht avatar Sep 09 '24 20:09 iamralpht

Hi,

I have repeated the tests with version "0.30.0-rc01".

The use case would be to have some generic button that can used multiple times in a screen by setting individual text, and callback events. The button should contain the states: enabled, disabled and pressed while keeping the individual text associated, and callback to each button. I have included a screenshot of the figma document and a MainActivity.kt with the summary of the example. The generic button is called "#button1" and three instances of the button are added to the screen, two of them replaced with ComponentReplacementContext and the third not.

In the example the button states works individually, but the "whilePressing" event defined the Figma prototype does not work.

Captura

MainActivity.kt

package com.example.designcomposenavtest

import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.android.designcompose.ComponentReplacementContext
import com.android.designcompose.DesignSettings
import com.android.designcompose.TapCallback
import com.android.designcompose.annotation.Design
import com.android.designcompose.annotation.DesignComponent
import com.android.designcompose.annotation.DesignDoc
import com.android.designcompose.annotation.DesignVariant
import timber.log.Timber

enum class ButtonState {
    disabled,
    enabled
}

@DesignDoc(id = "id_document")
interface StageFrame {
    @DesignComponent(node = "#stage1", isRoot = true)
    fun Stage1(
        @Design(node = "#button1-stage1") button1: @Composable (ComponentReplacementContext) -> Unit,
        @Design(node = "#button2-stage1") button2: @Composable (ComponentReplacementContext) -> Unit,
    )

    @DesignComponent(node = "#button1")
    fun ButtonVariants(
        @DesignVariant(property = "#statebutton") buttonState: ButtonState,
        @Design(node = "#textButton") textButton: String,
        @Design(node = "#button1") onTapCallback: TapCallback
    )
}

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setFigmaToken()
        DesignSettings.enableLiveUpdates(this)
        setContent {
            App()
        }
    }

    @Composable
    fun App() {

        //Button 1 States
        var buttonState1: ButtonState by remember { mutableStateOf(ButtonState.disabled) }
        var buttonText1: String by remember { mutableStateOf("Enabled b1") }

        //Button 2 States
        var buttonState2: ButtonState by remember { mutableStateOf(ButtonState.disabled) }
        var buttonText2: String by remember { mutableStateOf("Enabled b2") }

        StageFrameDoc.Stage1(
            button1 = {
                StageFrameDoc.ButtonVariants(
                    buttonState = buttonState1,
                    textButton = buttonText1,
                    onTapCallback = {
                        if (buttonState1 == ButtonState.enabled) {
                            buttonState1 = ButtonState.disabled
                            buttonText1 = "Disabled b1"
                        } else {
                            buttonState1 = ButtonState.enabled
                            buttonText1 = "Enabled b1"
                        }
                    }
                )
            },
            button2 = {
                StageFrameDoc.ButtonVariants(
                    buttonState = buttonState2,
                    textButton = buttonText2,
                    onTapCallback = {
                        if (buttonState2 == ButtonState.enabled) {
                            buttonState2 = ButtonState.disabled
                            buttonText2 = "Disabled b2"
                        } else {
                            buttonState2 = ButtonState.enabled
                            buttonText2 = "Enabled b2"
                        }
                    }
                )
            }
        )

        //Indicators of current status
        Text(
            modifier = Modifier.absoluteOffset(50.dp, 300.dp),
            text = "btn 1: $buttonText1",
            fontSize = 30.sp
        )
        Text(
            modifier = Modifier.absoluteOffset(50.dp, 400.dp),
            text = "btn 2: $buttonText2",
            fontSize = 30.sp
        )
    }

    private fun setFigmaToken() {
        val figmaAccessToken = "123"
        if (figmaAccessToken.isNotEmpty()) {
            val intent = Intent().apply {
                component =
                    ComponentName(
                        "com.example.designcomposenavtest",
                        "com.android.designcompose.ApiKeyService"
                    )
                action = "setApiKey"
                putExtra("ApiKey", figmaAccessToken)
            }
            startService(intent)
        } else {
            Timber.tag("MainActivity")
                .e("FIGMA_ACCESS_TOKEN is not defined. Skipping setting Figma token.")
        }
    }

}

miguel-galarza avatar Sep 10 '24 09:09 miguel-galarza

Hi @miguel-galarza, I was just looking through old tickets and noticed that we hadn't responded to this, sorry about that. Is this still an issue for you? Perhaps you need to pass in a unique key parameter to StageFrameDoc.ButtonVariants() to achieve the correct whilePressed behavior.

rylin8 avatar Feb 10 '25 16:02 rylin8

We're also seeing the "Tap on button stop working (TapCallback not triggered)" issue in 0.37.2 as mentioned in the very first post.

StephanSchuster avatar Oct 29 '25 20:10 StephanSchuster

Hi @miguel-galarza, I was just looking through old tickets and noticed that we hadn't responded to this, sorry about that. Is this still an issue for you? Perhaps you need to pass in a unique key parameter to StageFrameDoc.ButtonVariants() to achieve the correct whilePressed behavior.

Using "key" indeed fixes the "whilePressed" behavior. But I would rather see this as a workaround. If technically possible, I think this should work without "key".

StephanSchuster avatar Oct 31 '25 13:10 StephanSchuster