Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.itsaky.androidide.compose.preview.runtime

import androidx.compose.runtime.Composer
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Modifier as ReflectModifier
import kotlin.math.ceil

class PreviewSetupException(message: String, cause: Throwable? = null) : Exception(message, cause)

object ComposableInvoker {

fun findComposableMethod(clazz: Class<*>, functionName: String): Method? {
val methods = clazz.declaredMethods

methods.find { it.name == functionName }?.let {
it.isAccessible = true
return it
}

val candidates = methods.filter { method ->
!method.name.contains("\$default") &&
(method.name.startsWith("$functionName\$") || method.name == "${functionName}\$lambda")
}

return candidates.minByOrNull { it.parameterCount }?.also { it.isAccessible = true }
}

fun invokeSafely(clazz: Class<*>, method: Method, composer: Composer) {
val isStatic = ReflectModifier.isStatic(method.modifiers)

val instance = if (isStatic) {
null
} else {
try {
clazz.getDeclaredConstructor().newInstance()
} catch (e: Exception) {
throw PreviewSetupException("Failed to create instance for ${clazz.simpleName}", e)
}
}

if (!isStatic && instance == null) {
throw PreviewSetupException("Failed to create instance for ${clazz.simpleName}")
}

when (val signature = ComposeSignature.analyze(method)) {
is ComposeSignature.NoArgs -> executeInvocation { method.invoke(instance) }
is ComposeSignature.WithComposer -> invokeWithComposer(method, instance, signature, composer)
is ComposeSignature.Unsupported -> {
throw PreviewSetupException("Unsupported signature: ${signature.reason}")
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private fun invokeWithComposer(
method: Method,
instance: Any?,
signature: ComposeSignature.WithComposer,
composer: Composer
) {
val args = arrayOfNulls<Any>(signature.totalParams)
val realParamsCount = signature.composerIndex

for (i in 0 until realParamsCount) {
args[i] = getDefaultValue(signature.types[i])
}

args[signature.composerIndex] = composer

val changedInts = if (realParamsCount == 0) 1 else ceil(realParamsCount / COMPOSE_PARAMS_PER_CHANGED_INT).toInt()
val changedStartIndex = signature.composerIndex + 1
val changedEndIndex = minOf(changedStartIndex + changedInts, signature.totalParams)

args.fill(COMPOSE_CHANGED_EVALUATE_ALL, fromIndex = changedStartIndex, toIndex = changedEndIndex)
args.fill(COMPOSE_DEFAULT_USE_ALL_DEFAULTS, fromIndex = changedEndIndex, toIndex = signature.totalParams)

executeInvocation { method.invoke(instance, *args) }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private fun executeInvocation(action: () -> Unit) {
try {
action()
} catch (e: InvocationTargetException) {
throw e.targetException ?: e
} catch (e: Exception) {
throw PreviewSetupException("Reflection invocation failed", e)
}
}

private fun getDefaultValue(type: Class<*>): Any? {
if (!type.isPrimitive) return null
return when (type) {
Int::class.javaPrimitiveType -> 0
Boolean::class.javaPrimitiveType -> false
Float::class.javaPrimitiveType -> 0f
Double::class.javaPrimitiveType -> 0.0
Long::class.javaPrimitiveType -> 0L
Byte::class.javaPrimitiveType -> 0.toByte()
Short::class.javaPrimitiveType -> 0.toShort()
Char::class.javaPrimitiveType -> '\u0000'
else -> null
}
}

private const val COMPOSE_PARAMS_PER_CHANGED_INT = 10.0
private const val COMPOSE_CHANGED_EVALUATE_ALL = 0
private const val COMPOSE_DEFAULT_USE_ALL_DEFAULTS = -1
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand All @@ -18,7 +20,6 @@ import androidx.compose.ui.unit.dp
import org.slf4j.LoggerFactory
import java.io.File
import java.lang.reflect.Method
import java.lang.reflect.Modifier as ReflectModifier

class ComposableRenderer(
private val composeView: ComposeView,
Expand All @@ -39,79 +40,44 @@ class ComposableRenderer(
return
}

val composableMethod = findComposableMethod(clazz, functionName)
val composableMethod = ComposableInvoker.findComposableMethod(clazz, functionName)
if (composableMethod == null) {
showError("Composable function not found: $functionName")
return
}

composeView.setContent {
MaterialTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
RenderComposable(clazz, composableMethod)
try {
composeView.setContent {
val errorMessage = remember { mutableStateOf<String?>(null) }

MaterialTheme {
Surface(color = MaterialTheme.colorScheme.background) {
if (errorMessage.value != null) {
ErrorContent(message = errorMessage.value!!)
} else {
RenderComposable(clazz, composableMethod) { exception ->
val cause = exception.cause ?: exception
LOG.error("Reflection error before composition", cause)
errorMessage.value = "Setup failed: ${cause.message ?: cause.javaClass.simpleName}"
}
}
}
}
}
} catch (e: Exception) {
LOG.error("Preview crashed during initial composition", e)
showError("Preview crashed: ${e.cause?.message ?: e.message}")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

LOG.debug("Rendered composable: {}#{}", className, functionName)
}

private fun findComposableMethod(clazz: Class<*>, functionName: String): Method? {
val methods = clazz.declaredMethods

methods.find { it.name == functionName }?.let {
it.isAccessible = true
return it
}

val candidates = methods.filter { method ->
!method.name.contains("\$default") &&
(method.name.startsWith("$functionName\$") || method.name == "${functionName}\$lambda")
}

return candidates.minByOrNull { it.parameterCount }?.also { it.isAccessible = true }
}

@Composable
private fun RenderComposable(clazz: Class<*>, method: Method) {
val isStatic = ReflectModifier.isStatic(method.modifiers)
val instance = if (isStatic) {
null
} else {
runCatching { clazz.getDeclaredConstructor().newInstance() }.getOrNull()
}

if (!isStatic && instance == null) {
LOG.error("Failed to create instance for non-static method: {}", method.name)
ErrorContent("Failed to create instance for ${clazz.simpleName}")
return
}

private fun RenderComposable(clazz: Class<*>, method: Method, onReflectionError: (Exception) -> Unit) {
val composer = currentComposer
val paramCount = method.parameterCount

val invokeResult: Result<Any?> = when {
paramCount == 0 -> runCatching { method.invoke(instance) }
paramCount == 2 -> runCatching { method.invoke(instance, composer, 0) }
paramCount > 2 -> runCatching {
val args = arrayOfNulls<Any>(paramCount)
args[paramCount - 2] = composer
args[paramCount - 1] = 0
method.invoke(instance, *args)
}
else -> {
LOG.error("Unexpected parameter count {} for method: {}", paramCount, method.name)
ErrorContent("Unexpected parameter count: $paramCount")
return
}
}

if (invokeResult.isFailure) {
val e = invokeResult.exceptionOrNull()
LOG.error("Failed to invoke composable method: {}", method.name, e)
ErrorContent("Invocation failed: ${e?.message ?: "Unknown error"}")
try {
ComposableInvoker.invokeSafely(clazz, method, composer)
} catch (e: PreviewSetupException) {
onReflectionError(e)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.itsaky.androidide.compose.preview.runtime

import java.lang.reflect.Method

sealed class ComposeSignature {
object NoArgs : ComposeSignature()

class WithComposer(
val composerIndex: Int,
val totalParams: Int,
val types: Array<Class<*>>
) : ComposeSignature()

class Unsupported(val reason: String) : ComposeSignature()

companion object {
fun analyze(method: Method): ComposeSignature {
val types = method.parameterTypes
val paramCount = types.size

if (paramCount == 0) return NoArgs

val composerIndex = types.indexOfFirst { it.name == "androidx.compose.runtime.Composer" }

if (composerIndex == -1) {
return Unsupported("No Composer parameter found in ${method.name}")
}

for (i in (composerIndex + 1) until paramCount) {
if (types[i] != Int::class.javaPrimitiveType && types[i] != Integer::class.java) {
return Unsupported("Expected Int at index $i after Composer, but found ${types[i].simpleName}")
}
}

return WithComposer(composerIndex, paramCount, types)
}
}
}
Loading