Custom Components for Kotlin (Jetpack Compose)

This guide explains how to configure custom Jetpack Compose components in Locofy using the locofy.config.json file. This allows you to map your existing composables to Figma designs, enabling UIPro to generate Android code using your actual component library.

Configuration File: locofy.config.json

The locofy.config.json file defines how your Jetpack Compose components map to Figma components. It's automatically generated by UIPro when you run: "Create UIPro config for all components"

File Location

YourApp/
├── app/
│   └── src/
│       └── main/
│           └── java/
│               └── components/
├── locofy.config.json  ← Configuration file
└── build.gradle

Jetpack Compose Component Examples

Example 1: Button Component

Component Code:

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
 
enum class ButtonSize {
    SMALL, MEDIUM, LARGE
}
 
enum class ButtonVariant {
    PRIMARY, SECONDARY
}
 
@Composable
fun CustomButton(
    label: String,
    size: ButtonSize = ButtonSize.MEDIUM,
    variant: ButtonVariant = ButtonVariant.PRIMARY,
    enabled: Boolean = true,
    onClick: () -> Unit = {}
) {
    val padding = when (size) {
        ButtonSize.SMALL -> 6.dp to 12.dp
        ButtonSize.MEDIUM -> 10.dp to 16.dp
        ButtonSize.LARGE -> 14.dp to 24.dp
    }
    
    val fontSize = when (size) {
        ButtonSize.SMALL -> 14.sp
        ButtonSize.MEDIUM -> 16.sp
        ButtonSize.LARGE -> 18.sp
    }
    
    val backgroundColor = when (variant) {
        ButtonVariant.PRIMARY -> Color(0xFF007AFF)
        ButtonVariant.SECONDARY -> Color(0xFF5856D6)
    }
    
    Button(
        onClick = onClick,
        enabled = enabled,
        colors = ButtonDefaults.buttonColors(
            containerColor = backgroundColor
        ),
        modifier = Modifier.padding(horizontal = padding.second, vertical = padding.first)
    ) {
        Text(
            text = label,
            fontSize = fontSize,
            color = Color.White
        )
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./app/src/main/java/components/CustomButton.kt",
      "name": "CustomButton",
      "props": [
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "config": {
            "layerName": "[children]"
          }
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["SMALL", "MEDIUM", "LARGE"],
          "config": {
            "layerProp": "Size",
            "valueMapping": {
              "SMALL": ["small"],
              "MEDIUM": ["medium"],
              "LARGE": ["large"]
            }
          }
        },
        {
          "name": "variant",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["PRIMARY", "SECONDARY"],
          "config": {
            "layerProp": "Variant",
            "valueMapping": {
              "PRIMARY": ["primary"],
              "SECONDARY": ["secondary"]
            }
          }
        },
        {
          "name": "enabled",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Button"
      }
    }
  ],
  "projectId": "your-project-id",
  "projectName": "Your Android App"
}

Example 2: Card Component

Component Code:

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
 
@Composable
fun CardView(
    modifier: Modifier = Modifier,
    header: (@Composable () -> Unit)? = null,
    content: @Composable () -> Unit,
    footer: (@Composable () -> Unit)? = null
) {
    Card(
        modifier = modifier,
        colors = CardDefaults.cardColors(
            containerColor = Color.White
        ),
        elevation = CardDefaults.cardElevation(
            defaultElevation = 3.dp
        )
    ) {
        Column {
            if (header != null) {
                Box(modifier = Modifier.padding(16.dp)) {
                    header()
                }
                Divider(color = Color(0xFFE5E5EA))
            }
            
            Box(modifier = Modifier.padding(16.dp)) {
                content()
            }
            
            if (footer != null) {
                Divider(color = Color(0xFFE5E5EA))
                Box(modifier = Modifier.padding(16.dp)) {
                    footer()
                }
            }
        }
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./app/src/main/java/components/CardView.kt",
      "name": "CardView",
      "props": [
        {
          "name": "header",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Header"
          }
        },
        {
          "name": "content",
          "dataType": "node",
          "propType": 5,
          "isOptional": false,
          "config": {
            "layerName": "Content"
          }
        },
        {
          "name": "footer",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Footer"
          }
        }
      ],
      "config": {
        "layerName": "Card"
      }
    }
  ]
}

Example 3: TextField Component

Component Code:

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
 
enum class TextFieldSize {
    SMALL, MEDIUM, LARGE
}
 
@Composable
fun CustomTextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    placeholder: String = "",
    label: String? = null,
    error: String? = null,
    isPassword: Boolean = false,
    keyboardType: KeyboardType = KeyboardType.Text,
    size: TextFieldSize = TextFieldSize.MEDIUM
) {
    val padding = when (size) {
        TextFieldSize.SMALL -> 8.dp
        TextFieldSize.MEDIUM -> 12.dp
        TextFieldSize.LARGE -> 16.dp
    }
    
    val fontSize = when (size) {
        TextFieldSize.SMALL -> 14.sp
        TextFieldSize.MEDIUM -> 16.sp
        TextFieldSize.LARGE -> 18.sp
    }
    
    Column(modifier = modifier) {
        if (label != null) {
            Text(
                text = label,
                fontSize = 14.sp,
                color = Color(0xFF1C1C1E),
                modifier = Modifier.padding(bottom = 8.dp)
            )
        }
        
        TextField(
            value = value,
            onValueChange = onValueChange,
            placeholder = { Text(placeholder) },
            visualTransformation = if (isPassword) PasswordVisualTransformation() else VisualTransformation.None,
            keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
            isError = error != null,
            colors = TextFieldDefaults.colors(
                unfocusedContainerColor = Color(0xFFF2F2F7),
                focusedContainerColor = Color.White,
                errorContainerColor = Color(0xFFFFF0F0)
            ),
            modifier = Modifier.fillMaxWidth()
        )
        
        if (error != null) {
            Text(
                text = error,
                fontSize = 12.sp,
                color = Color.Red,
                modifier = Modifier.padding(top = 4.dp)
            )
        }
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./app/src/main/java/components/CustomTextField.kt",
      "name": "CustomTextField",
      "props": [
        {
          "name": "placeholder",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": true,
          "config": {
            "layerName": "Label"
          }
        },
        {
          "name": "error",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "isPassword",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["SMALL", "MEDIUM", "LARGE"],
          "config": {
            "layerProp": "Size",
            "valueMapping": {
              "SMALL": ["small", "sm"],
              "MEDIUM": ["medium", "md"],
              "LARGE": ["large", "lg"]
            }
          }
        }
      ],
      "config": {
        "layerName": "TextField"
      }
    }
  ]
}

Example 4: Avatar Component

Component Code:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
 
enum class AvatarSize {
    XS, SMALL, MEDIUM, LARGE, XL
}
 
enum class AvatarShape {
    CIRCLE, SQUARE
}
 
@Composable
fun AvatarView(
    imageUrl: String,
    modifier: Modifier = Modifier,
    size: AvatarSize = AvatarSize.MEDIUM,
    shape: AvatarShape = AvatarShape.CIRCLE,
    showBadge: Boolean = false
) {
    val dimension = when (size) {
        AvatarSize.XS -> 24.dp
        AvatarSize.SMALL -> 32.dp
        AvatarSize.MEDIUM -> 48.dp
        AvatarSize.LARGE -> 64.dp
        AvatarSize.XL -> 96.dp
    }
    
    val cornerRadius = when (shape) {
        AvatarShape.CIRCLE -> dimension / 2
        AvatarShape.SQUARE -> 8.dp
    }
    
    Box(
        modifier = modifier
            .size(dimension)
    ) {
        AsyncImage(
            model = imageUrl,
            contentDescription = "Avatar",
            modifier = Modifier
                .fillMaxSize()
                .clip(RoundedCornerShape(cornerRadius)),
            contentScale = ContentScale.Crop
        )
        
        if (showBadge) {
            Surface(
                modifier = Modifier
                    .size(12.dp)
                    .align(Alignment.BottomEnd),
                shape = CircleShape,
                color = Color(0xFF34C759),
                shadowElevation = 2.dp
            ) {}
        }
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./app/src/main/java/components/AvatarView.kt",
      "name": "AvatarView",
      "props": [
        {
          "name": "imageUrl",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "attr": "src"
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["XS", "SMALL", "MEDIUM", "LARGE", "XL"],
          "config": {
            "layerProp": "Size",
            "valueMapping": {
              "XS": ["xs"],
              "SMALL": ["small", "sm"],
              "MEDIUM": ["medium", "md"],
              "LARGE": ["large", "lg"],
              "XL": ["xl"]
            }
          }
        },
        {
          "name": "shape",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["CIRCLE", "SQUARE"],
          "config": {
            "layerProp": "Shape",
            "valueMapping": {
              "CIRCLE": ["circle"],
              "SQUARE": ["square"]
            }
          }
        },
        {
          "name": "showBadge",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Avatar"
      }
    }
  ]
}

Example 5: List Item Component

Component Code:

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
 
@Composable
fun ListItemView(
    title: String,
    modifier: Modifier = Modifier,
    subtitle: String? = null,
    leftIcon: (@Composable () -> Unit)? = null,
    rightIcon: (@Composable () -> Unit)? = null,
    onClick: (() -> Unit)? = null
) {
    Column {
        Row(
            modifier = modifier
                .fillMaxWidth()
                .clickable(enabled = onClick != null) { onClick?.invoke() }
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            if (leftIcon != null) {
                Box(modifier = Modifier.padding(end = 12.dp)) {
                    leftIcon()
                }
            }
            
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = title,
                    fontSize = 16.sp,
                    color = Color(0xFF1C1C1E)
                )
                
                if (subtitle != null) {
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(
                        text = subtitle,
                        fontSize = 14.sp,
                        color = Color(0xFF8E8E93)
                    )
                }
            }
            
            if (rightIcon != null) {
                Box(modifier = Modifier.padding(start = 12.dp)) {
                    rightIcon()
                }
            }
        }
        
        Divider(color = Color(0xFFE5E5EA))
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./app/src/main/java/components/ListItemView.kt",
      "name": "ListItemView",
      "props": [
        {
          "name": "leftIcon",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "LeftIcon"
          }
        },
        {
          "name": "title",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "config": {
            "layerName": "Title"
          }
        },
        {
          "name": "subtitle",
          "dataType": "string",
          "propType": 1,
          "isOptional": true,
          "config": {
            "layerName": "Subtitle"
          }
        },
        {
          "name": "rightIcon",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "RightIcon"
          }
        }
      ],
      "config": {
        "layerName": "ListItem"
      }
    }
  ]
}

Complete locofy.config.json Example

{
  "components": [
    {
      "path": "./app/src/main/java/components/CustomButton.kt",
      "name": "CustomButton",
      "props": [
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": false
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["SMALL", "MEDIUM", "LARGE"],
          "config": {
            "layerProp": "Size",
            "valueMapping": {
              "SMALL": ["small"],
              "MEDIUM": ["medium"],
              "LARGE": ["large"]
            }
          }
        },
        {
          "name": "variant",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["PRIMARY", "SECONDARY"],
          "config": {
            "layerProp": "Variant",
            "valueMapping": {
              "PRIMARY": ["primary"],
              "SECONDARY": ["secondary"]
            }
          }
        }
      ],
      "config": {
        "layerName": "Button"
      }
    },
    {
      "path": "./app/src/main/java/components/CardView.kt",
      "name": "CardView",
      "props": [
        {
          "name": "header",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Header"
          }
        },
        {
          "name": "content",
          "dataType": "node",
          "propType": 5,
          "isOptional": false,
          "config": {
            "layerName": "Content"
          }
        }
      ],
      "config": {
        "layerName": "Card"
      }
    },
    {
      "path": "./app/src/main/java/components/CustomTextField.kt",
      "name": "CustomTextField",
      "props": [
        {
          "name": "placeholder",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["SMALL", "MEDIUM", "LARGE"],
          "config": {
            "layerProp": "Size"
          }
        }
      ],
      "config": {
        "layerName": "TextField"
      }
    }
  ],
  "projectId": "abc123xyz",
  "projectName": "My Android App"
}

Configuration Properties Reference

PropertyTypeDescription
pathstringRelative path to .kt file
namestringComposable function name
propsarrayArray of parameter definitions
configobjectComponent-level configuration
targetTagsarrayReplace native composables

Jetpack Compose-Specific Considerations

1. Composable Content Lambdas

Composable lambdas map as node-type props:

{
  "name": "content",
  "dataType": "node",
  "propType": 5
}

2. State Management

MutableState and ViewModel properties are typically not mapped.

3. Enum Naming

Kotlin enums use UPPER_CASE naming. Use valueMapping to map to Figma:

{
  "name": "size",
  "expectValues": ["SMALL", "MEDIUM", "LARGE"],
  "config": {
    "valueMapping": {
      "SMALL": ["small"],
      "MEDIUM": ["medium"],
      "LARGE": ["large"]
    }
  }
}

4. Modifiers

Modifier parameters are typically not mapped in the config.


Advanced Features

Target Tags

Replace native Jetpack Compose components:

{
  "components": [
    {
      "path": "./app/src/main/java/components/Container.kt",
      "name": "Container",
      "targetTags": ["Column", "Row", "Box"],
      "props": []
    }
  ]
}

Workflow

  1. Create Jetpack Compose components with proper parameter definitions
  2. Run UIPro configuration: "Create UIPro config for all components"
  3. UIPro generates locofy.config.json
  4. Review configuration and adjust valueMapping for enums
  5. Open Figma and map components
  6. Generate Android code

Best Practices

  1. ✅ Use Kotlin enums for size and variant props
  2. ✅ Use composable lambdas for flexible content
  3. ✅ Provide default values for optional parameters
  4. ✅ Use Material 3 components when possible
  5. ✅ Use valueMapping for enum naming differences
  6. ✅ Keep composable functions simple and reusable
  7. ✅ Use proper state management (remember, mutableStateOf)

Next Steps