Skip to content

Commit

Permalink
[resources] Add functions to retrieve bytes from drawable or font res…
Browse files Browse the repository at this point in the history
…ources. (#4651)

Implemented two new experimental functions:
```kotlin
/**
 * Retrieves the byte array of the drawable resource.
 *
 * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
 * @param resource The drawable resource.
 * @return The byte array representing the drawable resource.
 */
@ExperimentalResourceApi
suspend fun getDrawableResourceBytes(
    environment: ResourceEnvironment,
    resource: DrawableResource
): ByteArray {...}

/**
 * Retrieves the byte array of the font resource.
 *
 * @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
 * @param resource The font resource.
 * @return The byte array representing the font resource.
 */
@ExperimentalResourceApi
suspend fun getFontResourceBytes(
    environment: ResourceEnvironment,
    resource: FontResource
): ByteArray {...}
```

fixes #4360
  • Loading branch information
terrakok committed Apr 23, 2024
1 parent d14b8f0 commit afe548b
Show file tree
Hide file tree
Showing 15 changed files with 261 additions and 56 deletions.
4 changes: 0 additions & 4 deletions components/README.md
Expand Up @@ -13,10 +13,6 @@ in Android Studio or in AppCode with [installed CocoaPods](https://kotlinlang.or
### Run JS in browser with WebAssembly Skia via Gradle:
`./gradlew :resources:demo:shared:jsBrowserDevelopmentRun`

### Run MacOS via Gradle:
- on Intel CPU: `./gradlew :resources:demo:shared:runDebugExecutableMacosX64`
- on Apple Silicon: `./gradlew :resources:demo:shared:runDebugExecutableMacosArm64`

# Tests
Run script:
```bash
Expand Down
2 changes: 1 addition & 1 deletion components/gradle.properties
Expand Up @@ -8,7 +8,7 @@ android.useAndroidX=true

#Versions
kotlin.version=1.9.23
compose.version=1.6.10-beta01
compose.version=1.6.10-dev1596
agp.version=8.2.2

#Compose
Expand Down
16 changes: 0 additions & 16 deletions components/resources/demo/shared/build.gradle.kts
Expand Up @@ -39,24 +39,8 @@ kotlin {
binaries.executable()
}

listOf(
macosX64(),
macosArm64()
).forEach { macosTarget ->
macosTarget.binaries {
executable {
entryPoint = "main"
}
}
}

applyDefaultHierarchyTemplate()
sourceSets {
all {
languageSettings {
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
}
}
val desktopMain by getting
val wasmJsMain by getting

Expand Down
Expand Up @@ -9,11 +9,16 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
import components.resources.demo.shared.generated.resources.droid_icon
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.getDrawableResourceBytes
import org.jetbrains.compose.resources.rememberResourceEnvironment

@OptIn(ExperimentalResourceApi::class)
@Composable
fun FileRes(paddingValues: PaddingValues) {
Column(
modifier = Modifier.padding(paddingValues)
modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState())
) {
Text(
modifier = Modifier.padding(16.dp),
Expand Down Expand Up @@ -48,6 +53,34 @@ fun FileRes(paddingValues: PaddingValues) {
Text(bytes.decodeToString())
""".trimIndent()
)
HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp))
OutlinedCard(
modifier = Modifier.padding(horizontal = 16.dp),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
val composeEnv = rememberResourceEnvironment()
var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) {
bytes = getDrawableResourceBytes(composeEnv, Res.drawable.droid_icon)
}
Text(
modifier = Modifier.padding(8.dp),
text = "droid_icon byte size = " + bytes.size,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Text(
modifier = Modifier.padding(16.dp),
text = """
val composeEnv = rememberResourceEnvironment()
var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) {
bytes = getDrawableResourceBytes(composeEnv, Res.drawable.droid_icon)
}
Text("droid_icon byte size = " + bytes.size)
""".trimIndent()
)
Text(
modifier = Modifier.padding(16.dp),
text = "File: 'files/platform-text.txt'",
Expand Down Expand Up @@ -80,5 +113,23 @@ fun FileRes(paddingValues: PaddingValues) {
Text(bytes.decodeToString())
""".trimIndent()
)
HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp))
OutlinedCard(
modifier = Modifier.padding(horizontal = 16.dp),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Text(
modifier = Modifier.padding(8.dp),
text = "File URI: " + Res.getUri("files/platform-text.txt"),
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Text(
modifier = Modifier.padding(16.dp),
text = """
Text("File URI: " + Res.getUri("files/platform-text.txt"))
""".trimIndent()
)
}
}

This file was deleted.

This file was deleted.

1 change: 1 addition & 0 deletions components/resources/library/build.gradle.kts
Expand Up @@ -53,6 +53,7 @@ kotlin {
optIn("kotlinx.cinterop.ExperimentalForeignApi")
optIn("kotlin.experimental.ExperimentalNativeApi")
optIn("org.jetbrains.compose.resources.InternalResourceApi")
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
}
}

Expand Down
Expand Up @@ -2,6 +2,7 @@ package org.jetbrains.compose.resources

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.font.*

/**
Expand Down Expand Up @@ -32,4 +33,20 @@ expect fun Font(
resource: FontResource,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal
): Font
): Font

/**
* Retrieves the byte array of the font resource.
*
* @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
* @param resource The font resource.
* @return The byte array representing the font resource.
*/
@ExperimentalResourceApi
suspend fun getFontResourceBytes(
environment: ResourceEnvironment,
resource: FontResource
): ByteArray {
val resourceItem = resource.getResourceItemByEnvironment(environment)
return DefaultResourceReader.read(resourceItem.path)
}
Expand Up @@ -95,7 +95,6 @@ internal expect fun SvgElement.toSvgPainter(density: Density): Painter

private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) }

@OptIn(ExperimentalResourceApi::class)
@Composable
private fun svgPainter(resource: DrawableResource): Painter {
val resourceReader = LocalResourceReader.current
Expand All @@ -110,6 +109,22 @@ private fun svgPainter(resource: DrawableResource): Painter {
return svgPainter
}

/**
* Retrieves the byte array of the drawable resource.
*
* @param environment The resource environment, which can be obtained from [rememberResourceEnvironment] or [getSystemResourceEnvironment].
* @param resource The drawable resource.
* @return The byte array representing the drawable resource.
*/
@ExperimentalResourceApi
suspend fun getDrawableResourceBytes(
environment: ResourceEnvironment,
resource: DrawableResource
): ByteArray {
val resourceItem = resource.getResourceItemByEnvironment(environment)
return DefaultResourceReader.read(resourceItem.path)
}

internal expect fun ByteArray.toImageBitmap(): ImageBitmap
internal expect fun ByteArray.toXmlElement(): Element
internal expect fun ByteArray.toSvgElement(): SvgElement
Expand Down
Expand Up @@ -43,7 +43,24 @@ fun pluralStringResource(resource: PluralStringResource, quantity: Int): String
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
suspend fun getPluralString(resource: PluralStringResource, quantity: Int): String =
loadPluralString(resource, quantity, DefaultResourceReader, getResourceEnvironment())
loadPluralString(resource, quantity, DefaultResourceReader, getSystemResourceEnvironment())

/**
* Loads a string using the specified string resource.
*
* @param environment The resource environment.
* @param resource The string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @return The loaded string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getPluralString(
environment: ResourceEnvironment,
resource: PluralStringResource,
quantity: Int
): String = loadPluralString(resource, quantity, DefaultResourceReader, environment)

private suspend fun loadPluralString(
resource: PluralStringResource,
Expand Down Expand Up @@ -99,9 +116,33 @@ suspend fun getPluralString(resource: PluralStringResource, quantity: Int, varar
resource, quantity,
formatArgs.map { it.toString() },
DefaultResourceReader,
getResourceEnvironment(),
getSystemResourceEnvironment(),
)

/**
* Loads a string using the specified string resource.
*
* @param environment The resource environment.
* @param resource The string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @param formatArgs The arguments to be inserted into the formatted string.
* @return The loaded string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getPluralString(
environment: ResourceEnvironment,
resource: PluralStringResource,
quantity: Int,
vararg formatArgs: Any
): String = loadPluralString(
resource, quantity,
formatArgs.map { it.toString() },
DefaultResourceReader,
environment
)

private suspend fun loadPluralString(
resource: PluralStringResource,
quantity: Int,
Expand Down
Expand Up @@ -5,12 +5,35 @@ import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.intl.Locale

internal data class ResourceEnvironment(
val language: LanguageQualifier,
val region: RegionQualifier,
val theme: ThemeQualifier,
val density: DensityQualifier
)
@ExperimentalResourceApi
class ResourceEnvironment internal constructor(
internal val language: LanguageQualifier,
internal val region: RegionQualifier,
internal val theme: ThemeQualifier,
internal val density: DensityQualifier
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as ResourceEnvironment

if (language != other.language) return false
if (region != other.region) return false
if (theme != other.theme) return false
if (density != other.density) return false

return true
}

override fun hashCode(): Int {
var result = language.hashCode()
result = 31 * result + region.hashCode()
result = 31 * result + theme.hashCode()
result = 31 * result + density.hashCode()
return result
}
}

internal interface ComposeEnvironment {
@Composable
Expand Down Expand Up @@ -39,14 +62,32 @@ internal val DefaultComposeEnvironment = object : ComposeEnvironment {
//ComposeEnvironment provider will be overridden for tests
internal val LocalComposeEnvironment = staticCompositionLocalOf { DefaultComposeEnvironment }

/**
* Returns an instance of [ResourceEnvironment].
*
* The [ResourceEnvironment] class represents the environment for resources.
*
* @return An instance of [ResourceEnvironment] representing the current environment.
*/
@ExperimentalResourceApi
@Composable
fun rememberResourceEnvironment(): ResourceEnvironment {
val composeEnvironment = LocalComposeEnvironment.current
return composeEnvironment.rememberEnvironment()
}

internal expect fun getSystemEnvironment(): ResourceEnvironment

//the function reference will be overridden for tests
//@TestOnly
internal var getResourceEnvironment = ::getSystemEnvironment

/**
* Provides the resource environment for non-composable access to string resources.
* Provides the resource environment for non-composable access to resources.
* It is an expensive operation! Don't use it in composable functions with no cache!
*/
internal var getResourceEnvironment = ::getSystemEnvironment
@ExperimentalResourceApi
fun getSystemResourceEnvironment(): ResourceEnvironment = getResourceEnvironment()

@OptIn(InternalResourceApi::class)
internal fun Resource.getResourceItemByEnvironment(environment: ResourceEnvironment): ResourceItem {
Expand Down
Expand Up @@ -46,7 +46,22 @@ fun stringArrayResource(resource: StringArrayResource): List<String> {
* @throws IllegalStateException if the string array with the given ID is not found.
*/
suspend fun getStringArray(resource: StringArrayResource): List<String> =
loadStringArray(resource, DefaultResourceReader, getResourceEnvironment())
loadStringArray(resource, DefaultResourceReader, getSystemResourceEnvironment())

/**
* Loads a list of strings using the specified string array resource.
*
* @param environment The resource environment.
* @param resource The string array resource to be used.
* @return A list of strings representing the items in the string array.
*
* @throws IllegalStateException if the string array with the given ID is not found.
*/
@ExperimentalResourceApi
suspend fun getStringArray(
environment: ResourceEnvironment,
resource: StringArrayResource
): List<String> = loadStringArray(resource, DefaultResourceReader, environment)

private suspend fun loadStringArray(
resource: StringArrayResource,
Expand Down

0 comments on commit afe548b

Please sign in to comment.