Hi, I want to read a json file from a resource folder in commonMain, how do I do this ? It's in a KM...
t
Hi, I want to read a json file from a resource folder in commonMain, how do I do this ? It's in a KMM library.
I did try to use moko-resources, but unfortunately it doesn't fit my case entirely, this library does exactly what I want but only for tests https://github.com/goncalossilva/kotlinx-resources
a
there’s not a clear way to do this as far as I know You could generate a .kt file that contains the JSON as a variable, by adapting this answer Or you could set an environment variable that points to the directory that contains the file, so when the process launches it can access the file (although this won’t work on Kotlin/JS in browsers)
j
Which targets do you support?
t
@Adam S yes generating a file with variables could be an option if it's not possible to read it in another way, thanks, @Jeff Lockhart it's a library for mobile so I only support Android and iOS for now
a
ohh if you’re only supporting a limited number of targets then you should be able to expect/actual some custom
loadResource()
function, so long as you know how to load a resource for each specific target?
t
But how do I use it in commonMain ? I don't want to read the resource on the platform itself, but I want to read it in commonMain, I'm not sure how expect/actual could be used in common code ?
j
There are a couple pieces to getting it working. First, you need to make sure the JSON file and any other resources from
commonMain/resources
are bundled with your app package. Second, you can create a common interface that both platforms can implement to then read the resource from the app bundle.
`expect`/`actual` allows you to define an API in commonMain, e.g.:
Copy code
expect fun getAsset(asset: String): Source?
and then implement that API in a platform-specific way for each of your supported platforms. E.g. in androidMain:
Copy code
actual fun getAsset(asset: String): Source = context.assets.open(asset).source()
and in iosMain:
Copy code
actual fun getAsset(asset: String): Source? {
    val dotIndex = asset.lastIndexOf('.')
    val name = asset.substring(0, dotIndex)
    val type = asset.substring(dotIndex + 1)
    val path = NSBundle.mainBundle
        .pathForResource(name, type)
        ?: return null
    return FileSystem.SYSTEM.source(path.toPath())
}
These are implementations in my project using Okio's Source as an input stream return type.
Obviously for Android, you need a reference to a
Context
in your implementation. Alternatively, you don't even need to use `expect`/`actual`. You can just define an interface and then inject a platform-specific implementation to use.
1
For the first part, actually getting the
commonMain/resources
in your application bundles, for Android I've just symlinked
androidMain/resources/assets
to
commonMain/resources
. iOS is the most complicated, as you need to hook into the gradle task that builds the framework to copy the resources into it. It depends how you're building the framework, which task this will be (CocoaPods plugin, XCFramework, fat framework, etc.). Here's some code I wrote to do this. Instead of creating a separate Gradle task, it's actually better practice to hook into the existing task with
doFirst
or
doLast
and perform the file copy there, so you don't have multiple tasks with the same output directory. I should update that code.
❤️ 1
If you are using the CocoaPods plugin, there's also this method you can use to copy the resources.
p
If it’s really just one json file, you can also just create a gradle task that reads the file and writes it to a
const val
to your commonMain source set
e
related to KT-42418, there's a lot of other issues linked from https://youtrack.jetbrains.com/issue/KT-49981/Multiplatform-API-to-access-library-and-application-resources and in the future we may have easier ways to handle resources
if it is a few small text files then generating code in Gradle is pretty easy, e.g. https://github.com/ephemient/aoc2021/blob/main/kt/build.gradle.kts#L67-L101
it just doesn't scale very well - the Kotlin compiler will get slower with more and larger generated sources
m
Why don’t you just use the already built in package from Jetbrains? My code fragment from commonMain:
Copy code
coroutineScope.launch {
	try {
		val configString = resource("config.json").readBytes().decodeToString()
		_masterModelState.update { s -> s.copy(config = format.decodeFromString(configString)) }
	} catch (e: Exception) {
		log.error(e) { "Could not read configuration resource file." }
	}
}
config.json
in
commonMain/resources/
resource
from package
org.jetbrains.compose.resources
json
from
Json
in package
kotlinx.serialization.json
This should work out of the box on Desktop, Android and iOS. I haven’t tried other platforms though.
c
The imageviewer example from the compose examples shows how to use resources in those three platforms very well. @Jeff Lockhart I am still stuck at trying to use resources on iOS without using cocoapods. Your code seems to work, I see my resources being copied to the framework in my build folder. However I am still unable to use them in iOS. Do I have to add some sort of "copy resources" build phase somewhere?
p
It doesn't have watchos support
r
You can add a build phase in the iOS project to copy the JSON file into the iOS file structure. Though it means supplying a path to the API you're using to load the file.
j
@Christian Gaisl sorry, the code I shared here assumes the resource is coming from the main application bundle (which works for this code running in tests). But if your resources are bundled within a shared Kotlin framework, you need to access them from the framework's bundle. So instead of
NSBundle.mainBundle
, use
NSBundle.bundleWithIdentifier("shared.framework.bundle.ID")
. The shared framework bundle ID should be the
{module.group}.{framework.name}
.
c
Thank you very much, @Jeff Lockhart. I know this is an old thread, but your responses were very helpful to me. I finally can now get JSON reading to work correctly in my project. 😊🙏🏼
❤️ 1
s
@Jeff Lockhart @Charles Prado Found the bundle id from Info.plist for the shared framework, but not much luck with reading the bundle. Not sure the format of the bundle id i understood from the above msg is correct. Can you help me here. This is the bundle identifier i found in Info.plist.
Copy code
<key>CFBundleIdentifier</key>
<string>com.example.abc.shared</string>
And i was using the below code to list all the files.
Copy code
NSBundle.bundleWithIdentifier("com.example.abc.shared.framework")?.pathsForResourcesOfType(
        inDirectory = rootFolder,
        ext = null
    ) as List<String>
}
j
Try removing the
.framework
suffix. The
CFBundleIdentifier
string itself should work.
s
Thanks for the prompt response. My bad, i also ended up building the framework as static, which was another reason why it didnt work.. Thank you so much
👍🏼 1
2227 Views