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.gradleJetpack 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
| Property | Type | Description |
|---|---|---|
path | string | Relative path to .kt file |
name | string | Composable function name |
props | array | Array of parameter definitions |
config | object | Component-level configuration |
targetTags | array | Replace 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
- Create Jetpack Compose components with proper parameter definitions
- Run UIPro configuration:
"Create UIPro config for all components" - UIPro generates
locofy.config.json - Review configuration and adjust valueMapping for enums
- Open Figma and map components
- Generate Android code
Best Practices
- ✅ Use Kotlin enums for size and variant props
- ✅ Use composable lambdas for flexible content
- ✅ Provide default values for optional parameters
- ✅ Use Material 3 components when possible
- ✅ Use valueMapping for enum naming differences
- ✅ Keep composable functions simple and reusable
- ✅ Use proper state management (remember, mutableStateOf)
Next Steps
- Check the main Custom Components guide
- Explore other guides: React Native, Swift
- Join our Discord community (opens in a new tab)