I am building a quiz app with jetpack compose and storing the questions in firestore by importing those questions through JSON file. I want some text Styling like italic, bold or underlined, and add add mathematical formula in code form in JSON file and render them exactly as they appear in the book to my app's UI.
I tried using using html tags and LaTeX libraries but only html tags worked but not the LaTeX for the compose?
Clarification: lets say i used this in firestore field for the questions field or any other field; what is Mitochondria(using html tags ? I want it to appear in my app as What is Mitochondria?(In italic) Or i used this in firestore: A & = \frac{\pi r^2}{2} \ & = \frac{1}{2} \pi r^2
And want it appear as this in my app; Area of Semi Circle
There are many ways to represent math formulas in text. In this answer, i’ll use a simple, natural text format to keep it easy to parse.
Here’s an example of how it looks:
You can use the ExpressionView
composable to display a math expression. For example:
ExpressionView(expression = "a^2 + 3")
This means you can save your math formulas as text in the format shown above and use ExpressionView
to render them when needed.
Demo:
Here’s the full code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
App(modifier = Modifier.padding(innerPadding))
}
}
}
}
@Composable
fun App(modifier: Modifier = Modifier) {
var text by remember { mutableStateOf("") }
Column(
modifier = modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
ExpressionView(
expression = text,
modifier = Modifier.background(color = Color.Yellow)
)
Spacer(modifier = Modifier.height(20.dp))
TextField(
value = text,
onValueChange = {
text = it
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp)
)
}
}
// this is the only api that should be used from the outside
@Composable
fun ExpressionView(
expression: String,
modifier: Modifier = Modifier,
) {
val expParser = remember { ExpParser() }
var rootNode = remember { mutableStateOf<AstNode?>(null) }
LaunchedEffect(expression) {
try {
rootNode.value = expParser.parse(expression)
} catch (ignored: Exception) {}
}
rootNode.value?.let {
RootNode(
rootNode = it,
modifier = modifier
)
}
}
// Abstract Syntax Tree Node
sealed class AstNode
data class Number(val value: String) : AstNode()
data class Identifier(val value: String) : AstNode()
data class BinaryOperation(val left: AstNode, val operator: Char, val right: AstNode) : AstNode()
data class SqrtNode(val argument: AstNode) : AstNode()
class ExpParser {
private var input: String = ""
private var pos: Int = 0
private fun currentChar(): Char {
return if (pos < input.length) input[pos] else '\u0000'
}
private fun advance() { pos++ }
private fun nextChar(): Char {
advance()
return currentChar()
}
fun parse(input: String): AstNode {
pos = 0
this.input = input
return parseExpression()
}
private fun parseExpression(): AstNode {
var node = parseTerm()
var c = currentChar()
val opSet = setOf('+', '-', '=')
while (opSet.contains(c)) {
advance()
node = BinaryOperation(node, c, parseTerm())
c = currentChar()
}
return node
}
private fun parseTerm(): AstNode {
var node = parseFactor()
var c = currentChar()
val opSet = setOf('*', '/', '^')
while (opSet.contains(c)) {
advance()
node = BinaryOperation(node, c, parseFactor())
c = currentChar()
}
return node
}
private fun parseFactor(): AstNode {
val c = currentChar()
return when (c) {
'(' -> {
advance() // consume '('
val node = parseExpression()
val c = currentChar()
require(c == ')')
advance()
node
}
in '0'..'9', '.' -> parseNumber()
else -> parseIdentifier()
}
}
private fun parseNumber(): AstNode {
var c = currentChar()
val number = buildString {
while (c.isDigit()) {
append(c)
c = nextChar()
}
}.toString()
return Number(number)
}
private fun parseIdentifier(): AstNode {
var c = currentChar()
val identifier = buildString {
while (c.isLetterOrDigit()) {
append(c)
c = nextChar()
}
}.toString()
return if ("sqrt" == identifier) {
SqrtNode(parseFactor())
} else {
Identifier(identifier)
}
}
}
@Composable
fun RootNode(rootNode: AstNode, modifier: Modifier = Modifier) {
when (rootNode) {
is Number -> {
Text(text = rootNode.value, modifier = modifier)
}
is BinaryOperation -> {
when (rootNode.operator) {
'+' -> Plus(
modifier = modifier,
left = { RootNode(rootNode.left, modifier) },
right = { RootNode(rootNode.right, modifier) }
)
'-' -> Minus(
modifier = modifier,
left = { RootNode(rootNode.left, modifier) },
right = { RootNode(rootNode.right, modifier) }
)
'*' -> Times(
modifier = modifier,
left = { RootNode(rootNode.left, modifier) },
right = { RootNode(rootNode.right, modifier) }
)
'/' -> Fraction(
modifier = modifier,
numerator = { RootNode(rootNode.left, modifier) },
denominator = { RootNode(rootNode.right, modifier) }
)
'=' -> Equal(
modifier = modifier,
left = { RootNode(rootNode.left, modifier) },
right = { RootNode(rootNode.right, modifier) }
)
'^' -> Power(
modifier = modifier,
base = { RootNode(rootNode.left, modifier) },
exponent = { RootNode(rootNode.right, modifier) }
)
}
}
is Identifier -> {
Text(text = rootNode.value, modifier = modifier)
}
is SqrtNode -> {
SquareRoot(
modifier = modifier,
content = { RootNode(rootNode.argument, modifier) }
)
}
}
}
@Composable
fun Fraction(
modifier: Modifier = Modifier,
fractionLineThickness: Dp = 1.dp,
fractionColor: Color = Color.Black,
numerator: @Composable () -> Unit,
denominator: @Composable () -> Unit
) {
var numeratorWidth by remember { mutableIntStateOf(0) }
var denominatorWidth by remember { mutableIntStateOf(0) }
val fractionWidth = remember(numeratorWidth, denominatorWidth) {
max(numeratorWidth, denominatorWidth)
}
val padding = 8.dp
// Measure the widest text (numerator or denominator)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier
) {
// Numerator
Box(modifier = Modifier.onSizeChanged { numeratorWidth = it.width }) {
numerator()
}
// Fraction line
HorizontalDivider(
modifier = Modifier
.width(padding + with(LocalDensity.current) { fractionWidth.toDp() }),
color = fractionColor,
thickness = fractionLineThickness
)
// Denominator
Box(modifier = Modifier.onSizeChanged { denominatorWidth = it.width }) {
denominator()
}
}
}
@Composable
private fun BinaryOperation(
op: String,
modifier: Modifier = Modifier,
withSpace: Boolean = true,
left: @Composable () -> Unit,
right: @Composable () -> Unit,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
left()
Text(text = if (withSpace) " $op " else op)
right()
}
}
@Composable
fun Equal(
modifier: Modifier = Modifier,
left: @Composable () -> Unit,
right: @Composable () -> Unit,
) = BinaryOperation(
modifier = modifier,
op = "=",
left = left,
right = right
)
@Composable
fun Minus(
modifier: Modifier = Modifier,
left: @Composable () -> Unit,
right: @Composable () -> Unit,
) = BinaryOperation(
modifier = modifier,
op = "-",
left = left,
right = right
)
@Composable
fun Plus(
modifier: Modifier = Modifier,
left: @Composable () -> Unit,
right: @Composable () -> Unit,
) = BinaryOperation(
modifier = modifier,
op = "+",
left = left,
right = right
)
@Composable
fun Times(
modifier: Modifier = Modifier,
left: @Composable () -> Unit,
right: @Composable () -> Unit,
) = BinaryOperation(
modifier = modifier,
op = "x",
left = left,
right = right
)
//x1=(b+-sqrt(b^2-4*a*c))/(2*a)
//y = a*
@Composable
fun Power(
modifier: Modifier = Modifier,
base: @Composable () -> Unit,
exponent: @Composable () -> Unit
) {
Row(modifier = modifier) {
Box(
modifier = Modifier
.alignBy { 0 }
) {
base()
}
Box(
modifier = Modifier
.alignBy { 0 }
) {
val textStyle = LocalTextStyle.current
CompositionLocalProvider(LocalTextStyle provides TextStyle(fontSize = textStyle.fontSize/2)) {
exponent()
}
}
}
}
@Composable
fun SquareRoot(
modifier: Modifier = Modifier,
color: Color = Color.Black,
content: @Composable () -> Unit
) {
var contentWidth by remember { mutableIntStateOf(0) }
var contentHeight by remember { mutableIntStateOf(0) }
val paddingLeft = remember(contentHeight) { contentHeight/6f }
val paddingTop = 2.dp
val paddingTopPx = with(LocalDensity.current) { paddingTop.toPx() }
val strokeWidth = with(LocalDensity.current) { 1.dp.toPx() }
Box(
modifier = modifier
) {
Canvas(
modifier = Modifier
.matchParentSize()
) {
val height = size.height
val width = size.width
val path = Path().apply {
moveTo(0f, 0.7f*height)
lineTo(paddingLeft/2, height)
lineTo(paddingLeft, paddingTopPx/2)
lineTo(width, paddingTopPx/2)
}
drawPath(
color = color,
path = path,
style = Stroke(
width = strokeWidth,
cap = StrokeCap.Round
),
)
}
Row {
Spacer(modifier = Modifier.width(with(LocalDensity.current) { paddingLeft.toDp() }))
Column {
Spacer(modifier = Modifier.height(paddingTop))
Box(
modifier = Modifier
.onSizeChanged {
contentWidth = it.width
contentHeight = it.height
}
) {
content()
}
}
}
}
}
Notes:
This is a basic example and may have some bugs or performance issues. You’ll need to fix those if they come up.
To keep the code short for this answer, I didn’t handle all edge cases. You might need to adjust it for your own needs.
I hope this helps you get started!