Skip to content

Commit

Permalink
Fix issue with nested typed/primitive preview params (#290)
Browse files Browse the repository at this point in the history
* [WIP] Fix issue with doubly typed preview params

* Add primitive param

* Update to use cleaner invoking and handle primitives

* Lint and cleanups

* rename and lint
  • Loading branch information
rbro112 authored Oct 23, 2024
1 parent bb0c904 commit b7fc589
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@file:Suppress("MagicNumber")

package com.emergetools.snapshots.sample.ui

import androidx.annotation.IntRange
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
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.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import com.emergetools.snapshots.sample.ui.theme.SnapshotsSampleTheme

private class CompletedIconPreviewProvider : PreviewParameterProvider<Int> {
override val values: Sequence<Int>
get() = sequenceOf(1, 2, 3, 4, 5)
}

@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
@Composable
fun CompletedIconPilePreview(
@PreviewParameter(CompletedIconPreviewProvider::class) completedWorkouts: Int,
) {
SnapshotsSampleTheme {
IconGroup(
completedCount = completedWorkouts,
)
}
}

@Composable
fun IconGroup(
@IntRange(from = 1) completedCount: Int,
) {
// Display up to 9 icons
val resultCompletedCount = completedCount.coerceAtMost(9)
val iconSize = 48.dp
val borderSize = 2.dp
val boxSize = iconSize + borderSize
val overlapSize = -1 * 20.dp

Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(overlapSize),
) {
repeat(resultCompletedCount) {
Box(
modifier = Modifier
.clip(CircleShape)
.background(Color.Blue)
.border(borderSize, Color.White, CircleShape)
.size(boxSize),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(com.emergetools.snapshots.sample.R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier.size(iconSize),
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.emergetools.snapshots.sample.ui

import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.emergetools.snapshots.sample.ui.theme.SnapshotsSampleTheme

@Composable
fun MultiUserRow(users: List<User>) {
Column {
users.forEach { user ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Profile picture
Surface(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.Cyan),
color = Color.Transparent
) {
// Center content vertically and horizontally
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = user.name.firstOrNull()?.uppercase() ?: "",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
}
}

Spacer(modifier = Modifier.width(16.dp))

// User details
Column {
Text(text = user.name, style = MaterialTheme.typography.bodyMedium)
Text(text = user.email, style = MaterialTheme.typography.bodySmall, color = Color.Gray)
}
}
}
}
}

@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun MultiUserRowPreview(
@PreviewParameter(MultiUserRowPreviewProvider::class) users: List<User>,
) {
SnapshotsSampleTheme {
MultiUserRow(users = users)
}
}

@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun MultiUserRowPreviewWithLimit(
@PreviewParameter(MultiUserRowPreviewProvider::class, limit = 2) users: List<User>,
) {
SnapshotsSampleTheme {
MultiUserRow(users = users)
}
}

class MultiUserRowPreviewProvider : PreviewParameterProvider<List<User>> {
override val values: Sequence<List<User>> = sequenceOf(
listOf(User(name = "Ryan", email = "[email protected]")),
listOf(
User(name = "Ryan", email = "[email protected]"),
User(name = "Trevor", email = "[email protected]"),
),
listOf(
User(name = "Ryan", email = "[email protected]"),
User(name = "Trevor", email = "[email protected]"),
User(name = "Josh", email = "[email protected]"),
),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.emergetools.snapshots.compose

import android.util.Log
import androidx.compose.runtime.reflect.ComposableMethod
import androidx.compose.runtime.reflect.getDeclaredComposableMethod
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import java.lang.reflect.ParameterizedType

/**
* Handles reflection-based invocation of Composable methods,
* including support for preview parameters.
*/
object ComposableInvoker {
private const val TAG = "ComposableInvoker"

/**
* Finds and returns the appropriate ComposableMethod for the given parameters.
*/
fun findComposableMethod(
klass: Class<*>,
methodName: String,
previewProviderClass: Class<out PreviewParameterProvider<*>>? = null
): ComposableMethod {
val composableMethod = previewProviderClass?.let { previewProvider ->
Log.d(TAG, "Looking for parameterized composable method: $methodName in class: ${klass.name}")

// Find the type argument for PreviewParameterProvider
val providerType = findPreviewParameterType(previewProvider)
?: throw IllegalArgumentException("Unable to determine type argument for PreviewParameterProvider")

// Try to find the method with either the primitive or wrapper type
tryGetComposableMethod(klass, methodName, providerType)
?: throw NoSuchMethodException("Could not find composable method: $methodName")
} ?: run {
Log.d(TAG, "Looking for composable method: $methodName in class: ${klass.name}")
klass.getDeclaredComposableMethod(methodName)
}
return composableMethod.also {
val backingMethod = composableMethod.asMethod()
if (!backingMethod.isAccessible) {
Log.i(TAG, "Marking composable method $methodName as accessible")
backingMethod.isAccessible = true
}
}
}

/**
* Gets parameters from a PreviewParameterProvider, respecting the limit if specified.
*/
fun getPreviewParameters(
parameterProviderClass: Class<out PreviewParameterProvider<*>>?,
limit: Int? = null
): List<Any?> {
return parameterProviderClass?.let {
val params = getPreviewProviderParameters(it)
limit?.let { maxLimit -> params.take(maxLimit) } ?: params
} ?: listOf(null)
}

private fun findPreviewParameterType(previewProviderClass: Class<*>): Class<*>? {
// Look through all interfaces implemented by the class
for (genericInterface in previewProviderClass.genericInterfaces) {
if (genericInterface is ParameterizedType &&
genericInterface.rawType == PreviewParameterProvider::class.java
) {
// Handle different types of type arguments
return when (val typeArg = genericInterface.actualTypeArguments.firstOrNull()) {
// Direct class reference
is Class<*> -> typeArg
// Nested generic type (e.g., List<User>)
is ParameterizedType -> typeArg.rawType as? Class<*>
// Other cases (wildcards, type variables, etc.)
else -> null
}
}
}

// Check superclass if the interface isn't found in the current class
val superclass = previewProviderClass.genericSuperclass
if (superclass is ParameterizedType) {
return findPreviewParameterType(superclass.rawType as Class<*>)
}

return null
}

@Suppress("SwallowedException")
private fun tryGetComposableMethod(
klass: Class<*>,
methodName: String,
parameterType: Class<*>
): ComposableMethod? {
// Map of primitive types to their Java object wrapper classes
val primitiveToWrapper: Map<Class<*>?, Class<*>?> = mapOf(
Int::class.javaPrimitiveType to Integer::class.java,
Long::class.javaPrimitiveType to Long::class.java,
Double::class.javaPrimitiveType to Double::class.java,
Float::class.javaPrimitiveType to Float::class.java,
Boolean::class.javaPrimitiveType to Boolean::class.java,
Byte::class.javaPrimitiveType to Byte::class.java,
Short::class.javaPrimitiveType to Short::class.java,
Char::class.javaPrimitiveType to Character::class.java
)

// Map of wrapper classes to their primitive types
val wrapperToPrimitive = primitiveToWrapper.entries.associate { (k, v) -> v to k }

return try {
// First try with the original type
klass.getDeclaredComposableMethod(methodName, parameterType)
} catch (e: NoSuchMethodException) {
Log.d(
TAG,
"Method $methodName not found with parameter type: $parameterType, trying primitive/wrapper type"
)
// If that fails, try with the corresponding primitive/wrapper type
val alternateType: Class<*>? = when {
parameterType.isPrimitive -> primitiveToWrapper[parameterType]
wrapperToPrimitive.containsKey(parameterType) -> wrapperToPrimitive[parameterType]
else -> null
}

alternateType?.let {
try {
klass.getDeclaredComposableMethod(methodName, it)
} catch (e: NoSuchMethodException) {
Log.d(
TAG,
"Method $methodName not found with primitive/wrapped type: $parameterType, returning null"
)
null
}
}
}
}

private fun getPreviewProviderParameters(
parameterProviderClass: Class<out PreviewParameterProvider<*>>
): List<Any?> {
val constructor = parameterProviderClass.constructors
.singleOrNull { it.parameterTypes.isEmpty() }
?.apply { isAccessible = true }
?: throw IllegalArgumentException(
"PreviewParameterProvider constructor can not have parameters"
)
val params = constructor.newInstance() as PreviewParameterProvider<*>
return params.values.toList()
}
}
Loading

0 comments on commit b7fc589

Please sign in to comment.