PreviewUtils.kt
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.tooling
import androidx.compose.ui.tooling.data.Group
import androidx.compose.ui.tooling.data.UiToolingDataApi
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
/**
* Tries to find the [Class] of the [PreviewParameterProvider] corresponding to the given FQN.
*/
internal fun String.asPreviewProviderClass(): Class<out PreviewParameterProvider<*>>? {
try {
@Suppress("UNCHECKED_CAST")
return Class.forName(this) as? Class<out PreviewParameterProvider<*>>
} catch (e: ClassNotFoundException) {
PreviewLogger.logError("Unable to find PreviewProvider '$this'", e)
return null
}
}
/**
* Returns an array with some values of a [PreviewParameterProvider]. If the given provider class
* is `null`, returns an empty array. Otherwise, if the given `parameterProviderIndex` is a valid
* index, returns a single-element array containing the value corresponding to that particular
* index in the provider's sequence. Finally, returns an array with all the values of the
* provider's sequence if `parameterProviderIndex` is invalid, e.g. negative.
*/
internal fun getPreviewProviderParameters(
parameterProviderClass: Class<out PreviewParameterProvider<*>>?,
parameterProviderIndex: Int
): Array<Any?> {
if (parameterProviderClass != null) {
try {
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<*>
if (parameterProviderIndex < 0) {
return params.values.toArray(params.count)
}
return arrayOf(params.values.elementAt(parameterProviderIndex))
} catch (e: KotlinReflectionNotSupportedError) {
// kotlin-reflect runtime dependency not found. Suggest adding it.
throw IllegalStateException(
"Deploying Compose Previews with PreviewParameterProvider " +
"arguments requires adding a dependency to the kotlin-reflect library.\n" +
"Consider adding 'debugImplementation " +
"\"org.jetbrains.kotlin:kotlin-reflect:\$kotlin_version\"' " +
"to the module's build.gradle."
)
}
} else {
return emptyArray()
}
}
@OptIn(UiToolingDataApi::class)
internal fun Group.firstOrNull(predicate: (Group) -> Boolean): Group? {
return findGroupsThatMatchPredicate(this, predicate, true).firstOrNull()
}
@OptIn(UiToolingDataApi::class)
internal fun Group.findAll(predicate: (Group) -> Boolean): List<Group> {
return findGroupsThatMatchPredicate(this, predicate)
}
/**
* Search [Group]s that match a given [predicate], starting from a given [root]. An optional
* boolean parameter can be set if we're interested in a single occurrence. If it's set, we
* return early after finding the first matching [Group].
*/
@OptIn(UiToolingDataApi::class)
private fun findGroupsThatMatchPredicate(
root: Group,
predicate: (Group) -> Boolean,
findOnlyFirst: Boolean = false
): List<Group> {
val result = mutableListOf<Group>()
val stack = mutableListOf(root)
while (stack.isNotEmpty()) {
val current = stack.removeLast()
if (predicate(current)) {
if (findOnlyFirst) {
return listOf(current)
}
result.add(current)
}
stack.addAll(current.children)
}
return result
}
private fun Sequence<Any?>.toArray(size: Int): Array<Any?> {
val iterator = iterator()
return Array(size) { iterator.next() }
}
/**
* A simple wrapper to store and throw exception later in a thread-safe way.
*/
internal class ThreadSafeException {
private var exception: Throwable? = null
/**
* A lock to take to access exception.
*/
private val lock = Any()
fun set(throwable: Throwable) {
synchronized(lock) {
exception = throwable
}
}
fun throwIfPresent() {
synchronized(lock) {
exception?.let {
exception = null
throw it
}
}
}
}