diff --git a/components/gradle/libs.versions.toml b/components/gradle/libs.versions.toml index 0a6d16ca83d..607c2dd1c5f 100644 --- a/components/gradle/libs.versions.toml +++ b/components/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] kotlinx-coroutines = "1.7.3" +kotlinx-io = "0.3.1" androidx-appcompat = "1.6.1" androidx-activity-compose = "1.8.2" androidx-test = "1.5.0" @@ -8,6 +9,7 @@ androidx-compose = "1.6.0" [libraries] kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } diff --git a/components/resources/demo/shared/build.gradle.kts b/components/resources/demo/shared/build.gradle.kts index fd6510bd452..74f055ffc59 100644 --- a/components/resources/demo/shared/build.gradle.kts +++ b/components/resources/demo/shared/build.gradle.kts @@ -59,6 +59,7 @@ kotlin { commonMain.dependencies { implementation(compose.runtime) implementation(compose.material3) + implementation(libs.kotlinx.io.core) implementation(project(":resources:library")) } val desktopMain by getting diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt index efe4b687374..b246cad41d4 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt @@ -9,11 +9,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import components.resources.demo.shared.generated.resources.Res +import kotlinx.io.* @Composable fun FileRes(paddingValues: PaddingValues) { Column( - modifier = Modifier.padding(paddingValues) + modifier = Modifier.padding(paddingValues).verticalScroll(rememberScrollState()) ) { Text( modifier = Modifier.padding(16.dp), @@ -80,5 +81,50 @@ fun FileRes(paddingValues: PaddingValues) { Text(bytes.decodeToString()) """.trimIndent() ) + Text( + modifier = Modifier.padding(16.dp), + text = "File: 'drawable/compose.png'", + style = MaterialTheme.typography.titleLarge + ) + OutlinedCard( + modifier = Modifier.padding(horizontal = 16.dp), + shape = RoundedCornerShape(4.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + var content by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + @OptIn(ExperimentalStdlibApi::class) + Buffer().use { buffer -> + Res.getAsFlow("drawable/compose.png").collect { chunk -> + buffer.write(chunk) + } + content = buffer.readByteArray().asList().toString() + } + } + Text( + modifier = Modifier.padding(8.dp).height(200.dp).verticalScroll(rememberScrollState()), + text = content, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + Text( + modifier = Modifier.padding(16.dp), + text = """ + import kotlinx.io.* + + var content by remember { + mutableStateOf("") + } + LaunchedEffect(Unit) { + Buffer().use { buffer -> + Res.getAsFlow("drawable/compose.png").collect { chunk -> + buffer.write(chunk) + } + content = buffer.readByteArray().asList().toString() + } + } + Text(content) + """.trimIndent() + ) } } \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt index b04857b4247..f88f906e5ec 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt +++ b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt @@ -1,18 +1,52 @@ package org.jetbrains.compose.resources +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import java.io.File +import java.io.IOException +import java.io.InputStream private object AndroidResourceReader @OptIn(ExperimentalResourceApi::class) @InternalResourceApi actual suspend fun readResourceBytes(path: String): ByteArray { + try { + return getResourceAsStream(path).readBytes() + } catch (e: IOException) { + throw ResourceIOException(e) + } +} + +@OptIn(ExperimentalResourceApi::class) +@InternalResourceApi +actual fun getResourceAsFlow(path: String, byteCount: Int): Flow { + check(byteCount > 0) { "byteCount: $byteCount" } + return flow { + try { + val resource = getResourceAsStream(path) + val buffer = ByteArray(byteCount) + resource.use { + var numBytesRead: Int + while (resource.read(buffer).also { numBytesRead = it } != -1) { + emit(buffer.sliceArray(0 until numBytesRead)) + } + } + } catch (e: IOException) { + throw ResourceIOException(e) + } + }.flowOn(Dispatchers.IO) +} + +@OptIn(ExperimentalResourceApi::class) +private fun getResourceAsStream(path: String): InputStream { val classLoader = Thread.currentThread().contextClassLoader ?: AndroidResourceReader.javaClass.classLoader - val resource = classLoader.getResourceAsStream(path) ?: run { + return classLoader.getResourceAsStream(path) ?: run { //try to find a font in the android assets if (File(path).parentFile?.name.orEmpty() == "font") { classLoader.getResourceAsStream("assets/$path") } else null } ?: throw MissingResourceException(path) - return resource.readBytes() } \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt index c8d7f9ac1a8..960fb9f642c 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt @@ -1,10 +1,17 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.staticCompositionLocalOf +import kotlinx.coroutines.flow.Flow @ExperimentalResourceApi class MissingResourceException(path: String) : Exception("Missing resource with path: $path") +@ExperimentalResourceApi +class ResourceIOException : Exception { + constructor(message: String?) : super(message) + constructor(cause: Throwable?) : super(cause) +} + /** * Reads the content of the resource file at the specified path and returns it as a byte array. * @@ -14,6 +21,29 @@ class MissingResourceException(path: String) : Exception("Missing resource with @InternalResourceApi expect suspend fun readResourceBytes(path: String): ByteArray +/** + * Returns a flow which emits the content of the resource file as byte array chunks. The length of each chunk is not + * empty and has the length of [byteCount] or smaller. The flow will throw [MissingResourceException] when the resource + * file is missing or [ResourceIOException] if any IO error occurs. You can catch those with the + * [catch][kotlinx.coroutines.flow.catch] operator. This function is useful when the resource is too big to be contained + * in a single [ByteArray]. + * + * @param path The path of the file to read in the resource's directory. + * @param byteCount The maximum length of the emitted byte arrays. The flow can emit an array smaller than this length. + * + * @return A flow that emits the content of the file as byte sub-arrays. + * + * @throws IllegalArgumentException When [byteCount] is not positive. + */ +@InternalResourceApi +expect fun getResourceAsFlow(path: String, byteCount: Int = DEFAULT_RESOURCE_CHUNK_SIZE): Flow + +/** + * The default size of byte array chunks emitted by flows built with [getResourceAsFlow]. + */ +@InternalResourceApi +const val DEFAULT_RESOURCE_CHUNK_SIZE: Int = 8192 + internal interface ResourceReader { suspend fun read(path: String): ByteArray } diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt index 61949ee8425..211e406f316 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.runComposeUiTest +import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.runTest import kotlin.test.* @@ -149,6 +150,9 @@ class ComposeResourceTest { assertFailsWith { readResourceBytes("missing.png") } + assertFailsWith { + getResourceAsFlow("missing.png").collect() + } val error = assertFailsWith { getString(TestStringResource("unknown_id")) } @@ -176,4 +180,11 @@ class ComposeResourceTest { bytes.decodeToString() ) } + + @Test + fun testGetFileResourceAsSource() = runTest { + val bytes = readResourceBytes("strings.xml") + val source = getResourceAsFlow("strings.xml").toList().flatMap { it.asList() } + assertContentEquals(bytes, source.toByteArray()) + } } diff --git a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt index 9d393b859a6..0e7ace9aa52 100644 --- a/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt +++ b/components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt @@ -1,11 +1,46 @@ package org.jetbrains.compose.resources +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.io.IOException +import java.io.InputStream + private object JvmResourceReader @OptIn(ExperimentalResourceApi::class) @InternalResourceApi actual suspend fun readResourceBytes(path: String): ByteArray { + try { + return getResourceAsStream(path).readBytes() + } catch (e: IOException) { + throw ResourceIOException(e) + } +} + +@OptIn(ExperimentalResourceApi::class) +@InternalResourceApi +actual fun getResourceAsFlow(path: String, byteCount: Int): Flow { + check(byteCount > 0) { "byteCount: $byteCount" } + return flow { + try { + val resource = getResourceAsStream(path) + val buffer = ByteArray(byteCount) + resource.use { + var numBytesRead: Int + while (resource.read(buffer).also { numBytesRead = it } != -1) { + emit(buffer.sliceArray(0 until numBytesRead)) + } + } + } catch (e: IOException) { + throw ResourceIOException(e) + } + }.flowOn(Dispatchers.IO) +} + +@OptIn(ExperimentalResourceApi::class) +private fun getResourceAsStream(path: String): InputStream { val classLoader = Thread.currentThread().contextClassLoader ?: JvmResourceReader.javaClass.classLoader - val resource = classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) - return resource.readBytes() + return classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) } \ No newline at end of file diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt index 0a81769a562..1a2b4286377 100644 --- a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt @@ -1,9 +1,17 @@ package org.jetbrains.compose.resources import kotlinx.cinterop.addressOf +import kotlinx.cinterop.reinterpret import kotlinx.cinterop.usePinned +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import platform.Foundation.NSBundle import platform.Foundation.NSFileManager +import platform.Foundation.NSInputStream +import platform.Foundation.inputStreamWithFileAtPath import platform.posix.memcpy @OptIn(ExperimentalResourceApi::class) @@ -18,4 +26,42 @@ actual suspend fun readResourceBytes(path: String): ByteArray { memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) } } +} + +@OptIn(ExperimentalResourceApi::class) +@InternalResourceApi +actual fun getResourceAsFlow(path: String, byteCount: Int): Flow { + check(byteCount > 0) { "byteCount: $byteCount" } + return flow { + val fileManager = NSFileManager.defaultManager() + // todo: support fallback path at bundle root? + val composeResourcesPath = NSBundle.mainBundle.resourcePath + "/compose-resources/" + path + val stream = fileManager.inputStreamAsPath(composeResourcesPath) ?: throw MissingResourceException(path) + try { + stream.open() + val buffer = ByteArray(byteCount) + while (true) { + val numBytesRead = buffer.usePinned { pinned -> + stream.read(pinned.addressOf(0).reinterpret(), byteCount.toULong()) + }.toInt() + when { + numBytesRead < 0 -> throw ResourceIOException( + stream.streamError?.localizedDescription ?: "Unknown error" + ) + + numBytesRead == 0 -> break + numBytesRead > 0 -> emit(buffer.sliceArray(0 until numBytesRead)) + } + } + } finally { + stream.close() + } + }.flowOn(Dispatchers.IO) +} + +private fun NSFileManager.inputStreamAsPath(path: String): NSInputStream? { + if (!isReadableFileAtPath(path)) { + return null + } + return NSInputStream.inputStreamWithFileAtPath(path) } \ No newline at end of file diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt index 8cfb63b243a..aaef2b91344 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt @@ -2,8 +2,12 @@ package org.jetbrains.compose.resources import kotlinx.browser.window import kotlinx.coroutines.await +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.ArrayBufferView import org.khronos.webgl.Int8Array +import kotlin.js.Promise private fun ArrayBuffer.toByteArray(): ByteArray = Int8Array(this, 0, byteLength).unsafeCast() @@ -17,4 +21,50 @@ actual suspend fun readResourceBytes(path: String): ByteArray { throw MissingResourceException(resPath) } return response.arrayBuffer().await().toByteArray() +} + +@OptIn(ExperimentalResourceApi::class) +@InternalResourceApi +actual fun getResourceAsFlow(path: String, byteCount: Int): Flow { + check(byteCount > 0) { "byteCount: $byteCount" } + return flow { + val resPath = WebResourcesConfiguration.getResourcePath(path) + val response = window.fetch(resPath).await() + if (!response.ok) { + throw MissingResourceException(resPath) + } + val body = response.body ?: throw MissingResourceException(resPath) + val bodyReader = body.getReader(js("""({ mode: "byob" })""")).unsafeCast() + var buffer = ArrayBuffer(byteCount) + while (true) { + val readResult = try { + bodyReader.read(Int8Array(buffer)).await() + } catch (e: Throwable) { + throw ResourceIOException(e) + } + val value = readResult.value + if (value != null) { + val array = value.unsafeCast() + if (array.isNotEmpty()) { + emit(array) + } + buffer = value.buffer + } + if (readResult.done) { + break + } + } + } +} + +/** + * Exposes the JavaScript [ReadableStreamBYOBReader](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader) to Kotlin + */ +private external interface ReadableStreamBYOBReader { + fun read(view: ArrayBufferView): Promise +} + +private external interface ReadableStreamBYOBReaderReadResult { + val value: ArrayBufferView? + val done: Boolean } \ No newline at end of file diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt index 906df6ae6cc..b08a4ad6be4 100644 --- a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt +++ b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt @@ -1,8 +1,16 @@ package org.jetbrains.compose.resources import kotlinx.cinterop.addressOf +import kotlinx.cinterop.reinterpret import kotlinx.cinterop.usePinned +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import platform.Foundation.NSFileManager +import platform.Foundation.NSInputStream +import platform.Foundation.inputStreamWithFileAtPath import platform.posix.memcpy @OptIn(ExperimentalResourceApi::class) @@ -21,4 +29,43 @@ actual suspend fun readResourceBytes(path: String): ByteArray { memcpy(it.addressOf(0), contentsAtPath.bytes, contentsAtPath.length) } } +} + +@OptIn(ExperimentalResourceApi::class) +@InternalResourceApi +actual fun getResourceAsFlow(path: String, byteCount: Int): Flow { + check(byteCount > 0) { "byteCount: $byteCount" } + return flow { + val currentDirectoryPath = NSFileManager.defaultManager().currentDirectoryPath + val stream = NSFileManager.defaultManager().run { + //todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain) + inputStreamAsPath("$currentDirectoryPath/src/macosMain/composeResources/$path") + ?: inputStreamAsPath("$currentDirectoryPath/src/macosTest/composeResources/$path") + ?: inputStreamAsPath("$currentDirectoryPath/src/commonMain/composeResources/$path") + ?: inputStreamAsPath("$currentDirectoryPath/src/commonTest/composeResources/$path") + } ?: throw MissingResourceException(path) + try { + stream.open() + val buffer = ByteArray(byteCount) + while (true) { + val numBytesRead = buffer.usePinned { pinned -> + stream.read(pinned.addressOf(0).reinterpret(), byteCount.toULong()) + }.toInt() + when { + numBytesRead == 0 -> break + numBytesRead > 0 -> emit(buffer.sliceArray(0 until numBytesRead)) + else -> throw ResourceIOException(stream.streamError?.description) + } + } + } finally { + stream.close() + } + }.flowOn(Dispatchers.IO) +} + +private fun NSFileManager.inputStreamAsPath(path: String): NSInputStream? { + if (!isReadableFileAtPath(path)) { + return null + } + return NSInputStream.inputStreamWithFileAtPath(path) } \ No newline at end of file diff --git a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt index efb80613b36..02ecedd54b2 100644 --- a/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt +++ b/components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt @@ -2,9 +2,13 @@ package org.jetbrains.compose.resources import kotlinx.browser.window import kotlinx.coroutines.await +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.ArrayBufferView import org.khronos.webgl.Int8Array import org.w3c.fetch.Response +import kotlin.js.Promise import kotlin.wasm.unsafe.UnsafeWasmMemoryApi import kotlin.wasm.unsafe.withScopedMemoryAllocator @@ -25,11 +29,46 @@ actual suspend fun readResourceBytes(path: String): ByteArray { return response.arrayBuffer().await().toByteArray() } -private fun ArrayBuffer.toByteArray(): ByteArray { +private fun ArrayBuffer.toByteArray(): ByteArray { val source = Int8Array(this, 0, byteLength) return jsInt8ArrayToKotlinByteArray(source) } +@OptIn(ExperimentalResourceApi::class) +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +@InternalResourceApi +actual fun getResourceAsFlow(path: String, byteCount: Int): Flow { + check(byteCount > 0) { "byteCount: $byteCount" } + return flow { + val resPath = WebResourcesConfiguration.getResourcePath(path) + val response = window.fetch(resPath).await() + if (!response.ok) { + throw MissingResourceException(resPath) + } + val body = response.body ?: throw MissingResourceException(resPath) + val bodyReader = (body as ReadableStream).getBYOBReader() + var buffer = ArrayBuffer(byteCount) + while (true) { + val readResult = try { + bodyReader.read(Int8Array(buffer)).await() + } catch (e: Throwable) { + throw ResourceIOException(e) + } + val value = readResult.value + if (value != null) { + val array = jsInt8ArrayToKotlinByteArray(value as Int8Array) + if (array.isNotEmpty()) { + emit(array) + } + buffer = value.buffer + } + if (readResult.done) { + break + } + } + } +} + @JsFun( """ (src, size, dstAddr) => { const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size); @@ -49,4 +88,29 @@ internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray { jsExportInt8ArrayToWasm(x, size, dstAddress) ByteArray(size) { i -> (memBuffer + i).loadByte() } } +} + +/** + * Exposes the JavaScript [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) to Kotlin + */ +private external interface ReadableStream : JsAny { + fun getReader(options: JsAny): T +} + +private fun byobReaderOption(): JsAny = js("""({ mode: "byob" })""") + +private fun ReadableStream.getBYOBReader(): ReadableStreamBYOBReader { + return getReader(byobReaderOption()) +} + +/** + * Exposes the JavaScript [ReadableStreamBYOBReader](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader) to Kotlin + */ +private external interface ReadableStreamBYOBReader : JsAny { + fun read(view: ArrayBufferView): Promise +} + +private external interface ReadableStreamBYOBReaderReadResult : JsAny { + val value: ArrayBufferView? + val done: Boolean } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt index 100291d8663..1161d7fc07d 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt @@ -1,6 +1,7 @@ package org.jetbrains.compose.resources import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter import org.jetbrains.compose.internal.utils.uppercaseFirstChar import java.nio.file.Path import java.util.* @@ -151,6 +152,46 @@ internal fun getResFileSpecs( .addStatement("""return %M("$moduleDir" + path)""", readResourceBytes) .build() ) + + val defaultResourceChunkSize = MemberName( + "org.jetbrains.compose.resources", "DEFAULT_RESOURCE_CHUNK_SIZE" + ) + val getResourceAsFlow = MemberName("org.jetbrains.compose.resources", "getResourceAsFlow") + resObject.addFunction( + FunSpec.builder("getAsFlow") + .addKdoc( + """ + Returns a flow which emits the content of the resource file as byte array chunks. The length of each + chunk is not empty and has the length of [byteCount] or smaller. The flow will throw + [MissingResourceException][org.jetbrains.compose.resources.MissingResourceException] when the + resource file is missing or [ResourceIOException][org.jetbrains.compose.resources.ResourceIOException] + if any IO error occurs. You can catch those with the [catch][kotlinx.coroutines.flow.catch] operator. + This function is useful when the resource is too big to be contained in a single [ByteArray]. + + Example: `val bytes = Res.getAsFlow("files/key.bin").toList().flatMap { it.asList() }` + + @param path The path of the file to read in the resource's directory. + @param byteCount The maximum length of the emitted byte arrays. The flow can emit an array smaller than this length. + + @return A flow that emits the content of the file as byte sub-arrays. + + @throws IllegalArgumentException When [byteCount] is not positive. + """.trimIndent() + ) + .addParameter("path", String::class) + .addParameter( + ParameterSpec.builder("byteCount", Int::class) + .defaultValue("%M", defaultResourceChunkSize) + .build() + ) + .returns( + ClassName("kotlinx.coroutines.flow", "Flow") + .plusParameter(ByteArray::class.asTypeName()) + ) + .addStatement("""return %M("$moduleDir" + path, byteCount)""", getResourceAsFlow) + .build() + ) + ResourceType.values().forEach { type -> resObject.addType(TypeSpec.objectBuilder(type.typeName).build()) }