I tried reporting this as a bug to Google's issue tracker but it was ignored and responded to me that it's intended behavior by referencing W3 standards and they don't fix it since this behavior is the same for Text views, however, in the same link to the W3 standards there are two appropriate examples required by them but Android's team choose the simpler example to implement.


I understand this is an edge case and even W3 standards indicated that but it can be implemented. So I implemented it by adding U+0640 ـ ARABIC TATWEEL
between certain characters by demand of the letterSpacing parameter, the result and code are not perfect but it works.
@Composable
fun LetterSpacedPersianText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = 14.sp,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
val isPersian = remember { Regex("[ء-ی]") }
val keshides = letterSpacing.value.toInt()
val keshidesText = buildString { repeat(keshides) { append('ـ') } }
val spacesText = buildAnnotatedString {
repeat(keshides) {
withStyle(style = SpanStyle(fontSize = fontSize.div(50 * letterSpacing.value))) {
append(' ')
}
}
}
if (isPersian.containsMatchIn(text)) {
if (text.length != 1) {
val totalCursive = "ئبپتثجچحخسشصضطظعغفقکگلمنهی"
val finalCursive = "أؤدذرزژو"
val newText = buildAnnotatedString {
var i = 0
while (i < text.length) {
val current = text[i]
append(current)
val next = text.getOrNull(i + 1)
if (next != null) {
if (totalCursive.contains(current) && totalCursive.contains(next) ||
totalCursive.contains(current) && finalCursive.contains(next)
) {
append(keshidesText)
}
if (current !in totalCursive && current !in finalCursive && next !in totalCursive && next !in finalCursive) {
append(spacesText)
}
}
i++
}
}
Text(
text = newText,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
} else {
Text(
text = text,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
}
} else {
Text(
text = text,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
}
}
Example:

@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "درود از ایران",
letterSpacing = 10.sp
)
}

@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "درود از ایران",
letterSpacing = 1.sp
)
}

@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "بسیار عالی",
letterSpacing = 1.sp
)
}

@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "بسیار عالی",
letterSpacing = 10.sp
)
}