Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XML resource optimizations #4559

Merged
merged 16 commits into from
Apr 3, 2024
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
5 changes: 5 additions & 0 deletions components/resources/demo/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ android {
compose.experimental {
web.application {}
}

//because the dependency on the compose library is a project dependency
compose.resources {
generateResClass = always
}
1 change: 0 additions & 1 deletion components/resources/demo/shared/gradle.properties

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,6 @@ fun StringRes(paddingValues: PaddingValues) {
Column(
modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState())
) {
Text(
modifier = Modifier.padding(16.dp),
text = "values/strings.xml",
style = MaterialTheme.typography.titleLarge
)
OutlinedCard(
modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) {
bytes = Res.readBytes("values/strings.xml")
}
Text(
modifier = Modifier.padding(8.dp),
text = bytes.decodeToString(),
color = MaterialTheme.colorScheme.onPrimaryContainer,
softWrap = false
)
}
OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = stringResource(Res.string.app_name),
Expand Down Expand Up @@ -89,9 +68,9 @@ fun StringRes(paddingValues: PaddingValues) {
)
OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = stringArrayResource(Res.string.str_arr).toString(),
value = stringArrayResource(Res.array.str_arr).toString(),
onValueChange = {},
label = { Text("Text(stringArrayResource(Res.string.str_arr).toString())") },
label = { Text("Text(stringArrayResource(Res.array.str_arr).toString())") },
enabled = false,
colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
Expand Down
1 change: 1 addition & 0 deletions components/resources/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ kotlin {
optIn("kotlin.RequiresOptIn")
optIn("kotlinx.cinterop.ExperimentalForeignApi")
optIn("kotlin.experimental.ExperimentalNativeApi")
optIn("org.jetbrains.compose.resources.InternalResourceApi")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import androidx.compose.ui.text.font.*
@Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val path = remember(environment) { resource.getPathByEnvironment(environment) }
val path = remember(environment) { resource.getResourceItemByEnvironment(environment).path }
return Font(path, LocalContext.current.assets, weight, style)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.content.res.Configuration
import android.content.res.Resources
import java.util.*

@OptIn(InternalResourceApi::class)
internal actual fun getSystemEnvironment(): ResourceEnvironment {
val locale = Locale.getDefault()
val configuration = Resources.getSystem().configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
package org.jetbrains.compose.resources

import java.io.File
import java.io.InputStream

private object AndroidResourceReader
internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader {
override suspend fun read(path: String): ByteArray {
val resource = getResourceAsStream(path)
return resource.readBytes()
}

@OptIn(ExperimentalResourceApi::class)
@InternalResourceApi
actual suspend fun readResourceBytes(path: String): ByteArray {
val classLoader = Thread.currentThread().contextClassLoader ?: AndroidResourceReader.javaClass.classLoader
val resource = classLoader.getResourceAsStream(path) ?: run {
//try to find a font in the android assets
if (File(path).parentFile?.name.orEmpty().startsWith("font")) {
classLoader.getResourceAsStream("assets/$path")
} else null
} ?: throw MissingResourceException(path)
return resource.readBytes()
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
val resource = getResourceAsStream(path)
val result = ByteArray(size.toInt())
resource.use { input ->
input.skip(offset)
input.read(result, 0, size.toInt())
}
return result
}

@OptIn(ExperimentalResourceApi::class)
private fun getResourceAsStream(path: String): InputStream {
val classLoader = Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader
val resource = classLoader.getResourceAsStream(path) ?: run {
//try to find a font in the android assets
if (File(path).parentFile?.name.orEmpty().startsWith("font")) {
classLoader.getResourceAsStream("assets/$path")
} else null
} ?: throw MissingResourceException(path)
return resource
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import androidx.compose.ui.text.font.*
*
* @see Resource
*/
@OptIn(InternalResourceApi::class)
@ExperimentalResourceApi
@Immutable
class FontResource
Expand All @@ -24,11 +23,10 @@ class FontResource
* @param path The path to the font resource file.
* @return A new [FontResource] object.
*/
@OptIn(InternalResourceApi::class)
@ExperimentalResourceApi
fun FontResource(path: String): FontResource = FontResource(
id = "FontResource:$path",
items = setOf(ResourceItem(emptySet(), path))
items = setOf(ResourceItem(emptySet(), path, -1, -1))
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import org.jetbrains.compose.resources.vector.xmldom.Element
* @param id The unique identifier of the drawable resource.
* @param items The set of resource items associated with the image resource.
*/
@OptIn(InternalResourceApi::class)
@ExperimentalResourceApi
@Immutable
class DrawableResource
Expand All @@ -32,11 +31,10 @@ class DrawableResource
* @param path The path of the drawable resource.
* @return An [DrawableResource] object.
*/
@OptIn(InternalResourceApi::class)
@ExperimentalResourceApi
fun DrawableResource(path: String): DrawableResource = DrawableResource(
id = "DrawableResource:$path",
items = setOf(ResourceItem(emptySet(), path))
items = setOf(ResourceItem(emptySet(), path, -1, -1))
)

/**
Expand All @@ -50,7 +48,7 @@ fun DrawableResource(path: String): DrawableResource = DrawableResource(
@Composable
fun painterResource(resource: DrawableResource): Painter {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val filePath = remember(resource, environment) { resource.getPathByEnvironment(environment) }
val filePath = remember(resource, environment) { resource.getResourceItemByEnvironment(environment).path }
val isXml = filePath.endsWith(".xml", true)
if (isXml) {
return rememberVectorPainter(vectorResource(resource))
Expand All @@ -72,7 +70,7 @@ private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) }
fun imageResource(resource: DrawableResource): ImageBitmap {
val resourceReader = LocalResourceReader.current
val imageBitmap by rememberResourceState(resource, { emptyImageBitmap }) { env ->
val path = resource.getPathByEnvironment(env)
val path = resource.getResourceItemByEnvironment(env).path
val cached = loadImage(path, resourceReader) {
ImageCache.Bitmap(it.toImageBitmap())
} as ImageCache.Bitmap
Expand All @@ -97,7 +95,7 @@ fun vectorResource(resource: DrawableResource): ImageVector {
val resourceReader = LocalResourceReader.current
val density = LocalDensity.current
val imageVector by rememberResourceState(resource, { emptyImageVector }) { env ->
val path = resource.getPathByEnvironment(env)
val path = resource.getResourceItemByEnvironment(env).path
val cached = loadImage(path, resourceReader) {
ImageCache.Vector(it.toXmlElement().toImageVector(density))
} as ImageCache.Vector
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.jetbrains.compose.resources

import androidx.compose.runtime.*
import org.jetbrains.compose.resources.plural.PluralCategory
import org.jetbrains.compose.resources.plural.PluralRuleList

/**
* Represents a quantity string resource in the application.
*
* @param id The unique identifier of the resource.
* @param key The key used to retrieve the string resource.
* @param items The set of resource items associated with the string resource.
*/
@ExperimentalResourceApi
@Immutable
class PluralStringResource
@InternalResourceApi constructor(id: String, val key: String, items: Set<ResourceItem>) : Resource(id, items)

/**
* Retrieves the string for the pluralization for the given quantity using the specified quantity string resource.
*
* @param resource The quantity string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @return The retrieved string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
@Composable
fun pluralStringResource(resource: PluralStringResource, quantity: Int): String {
val resourceReader = LocalResourceReader.current
val pluralStr by rememberResourceState(resource, quantity, { "" }) { env ->
loadPluralString(resource, quantity, resourceReader, env)
}
return pluralStr
}

/**
* Loads a string using the specified string resource.
*
* @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(resource: PluralStringResource, quantity: Int): String =
loadPluralString(resource, quantity, DefaultResourceReader, getResourceEnvironment())

@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
private suspend fun loadPluralString(
resource: PluralStringResource,
quantity: Int,
resourceReader: ResourceReader,
environment: ResourceEnvironment
): String {
val resourceItem = resource.getResourceItemByEnvironment(environment)
val item = getStringItem(resourceItem, resourceReader) as StringItem.Plurals
val pluralRuleList = PluralRuleList.getInstance(
environment.language,
environment.region,
)
val pluralCategory = pluralRuleList.getCategory(quantity)
val str = item.items[pluralCategory]
?: item.items[PluralCategory.OTHER]
?: error("Quantity string ID=`${resource.key}` does not have the pluralization $pluralCategory for quantity $quantity!")
return str
}

/**
* Retrieves the string for the pluralization for the given quantity using the specified quantity string resource.
*
* @param resource The quantity 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 retrieved string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
@Composable
fun pluralStringResource(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String {
val resourceReader = LocalResourceReader.current
val args = formatArgs.map { it.toString() }
val pluralStr by rememberResourceState(resource, quantity, args, { "" }) { env ->
loadPluralString(resource, quantity, args, resourceReader, env)
}
return pluralStr
}

/**
* Loads a string using the specified string resource.
*
* @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(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String =
loadPluralString(
resource, quantity,
formatArgs.map { it.toString() },
DefaultResourceReader,
getResourceEnvironment(),
)

@OptIn(ExperimentalResourceApi::class)
private suspend fun loadPluralString(
resource: PluralStringResource,
quantity: Int,
args: List<String>,
resourceReader: ResourceReader,
environment: ResourceEnvironment
): String {
val str = loadPluralString(resource, quantity, resourceReader, environment)
return str.replaceWithArgs(args)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ sealed class Resource
*
* @property qualifiers The qualifiers of the resource item.
* @property path The path of the resource item.
* @property offset The offset in bytes of the resource in the file. '-1' means the resource is whole file
* @property size The size in bytes of the resource in the file. '-1' means the resource is whole file
*/
@InternalResourceApi
@Immutable
data class ResourceItem(
internal val qualifiers: Set<Qualifier>,
internal val path: String
internal val path: String,
internal val offset: Long,
internal val size: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.intl.Locale

@OptIn(InternalResourceApi::class)
internal data class ResourceEnvironment(
val language: LanguageQualifier,
val region: RegionQualifier,
Expand All @@ -18,7 +17,6 @@ internal interface ComposeEnvironment {
fun rememberEnvironment(): ResourceEnvironment
}

@OptIn(InternalResourceApi::class)
internal val DefaultComposeEnvironment = object : ComposeEnvironment {
@Composable
override fun rememberEnvironment(): ResourceEnvironment {
Expand Down Expand Up @@ -51,17 +49,17 @@ internal expect fun getSystemEnvironment(): ResourceEnvironment
internal var getResourceEnvironment = ::getSystemEnvironment

@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
internal fun Resource.getPathByEnvironment(environment: ResourceEnvironment): String {
internal fun Resource.getResourceItemByEnvironment(environment: ResourceEnvironment): ResourceItem {
//Priority of environments: https://developer.android.com/guide/topics/resources/providing-resources#table2
items.toList()
.filterBy(environment.language)
.also { if (it.size == 1) return it.first().path }
.also { if (it.size == 1) return it.first() }
.filterBy(environment.region)
.also { if (it.size == 1) return it.first().path }
.also { if (it.size == 1) return it.first() }
.filterBy(environment.theme)
.also { if (it.size == 1) return it.first().path }
.also { if (it.size == 1) return it.first() }
.filterBy(environment.density)
.also { if (it.size == 1) return it.first().path }
.also { if (it.size == 1) return it.first() }
.let { items ->
if (items.isEmpty()) {
error("Resource with ID='$id' not found")
Expand All @@ -71,7 +69,6 @@ internal fun Resource.getPathByEnvironment(environment: ResourceEnvironment): St
}
}

@OptIn(InternalResourceApi::class)
private fun List<ResourceItem>.filterBy(qualifier: Qualifier): List<ResourceItem> {
//Android has a slightly different algorithm,
//but it provides the same result: https://developer.android.com/guide/topics/resources/providing-resources#BestMatch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ class MissingResourceException(path: String) : Exception("Missing resource with
* @return The content of the file as a byte array.
*/
@InternalResourceApi
expect suspend fun readResourceBytes(path: String): ByteArray
suspend fun readResourceBytes(path: String): ByteArray = DefaultResourceReader.read(path)

internal interface ResourceReader {
suspend fun read(path: String): ByteArray
suspend fun readPart(path: String, offset: Long, size: Long): ByteArray
}

internal val DefaultResourceReader: ResourceReader = object : ResourceReader {
@OptIn(InternalResourceApi::class)
override suspend fun read(path: String): ByteArray = readResourceBytes(path)
}
internal expect fun getPlatformResourceReader(): ResourceReader

internal val DefaultResourceReader = getPlatformResourceReader()

//ResourceReader provider will be overridden for tests
internal val LocalResourceReader = staticCompositionLocalOf { DefaultResourceReader }
Loading
Loading