Kotlin Multiplatform: use Android specific libs inside 'androidMain' - android

I followed the Targeting iOS and Android with Kotlin Multiplatform tutorial and was able to setup the working project for both ios and android.
As a next step I want to use android specific libs inside src/androidMain/. Example:
package com.test.android
import android.os.Build
actual fun getBuildInfo(): String {
return Build.MODEL
}
But for the import I receive: Unresolved reverence: os
Question: Which additional steps are needed to use android specific libs inside androidMain?
The: build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform")
}
kotlin {
//select iOS target platform depending on the Xcode environment variables
val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
::iosArm64
else
::iosX64
iOSTarget("ios") {
binaries {
framework {
baseName = "multi_module_be_connection"
}
}
}
jvm("android")
sourceSets["commonMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
}
sourceSets["androidMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
}
}
val packForXcode by tasks.creating(Sync::class) {
val targetDir = File(buildDir, "xcode-frameworks")
/// selecting the right configuration for the iOS
/// framework depending on the environment
/// variables set by Xcode build
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val framework = kotlin.targets
.getByName<KotlinNativeTarget>("ios")
.binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
from({ framework.outputDirectory })
into(targetDir)
/// generate a helpful ./gradlew wrapper with embedded Java path
doLast {
val gradlew = File(targetDir, "gradlew")
gradlew.writeText("#!/bin/bash\n"
+ "export 'JAVA_HOME=${System.getProperty("java.home")}'\n"
+ "cd '${rootProject.rootDir}'\n"
+ "./gradlew \$#\n")
gradlew.setExecutable(true)
}
}
tasks.getByName("build").dependsOn(packForXcode)
Source of the project is here: https://github.com/kotlin-hands-on/mpp-ios-android

In order to make use of android specific API in a MPP module you can include the android lib plugin:
The: build.gradle.kts
plugins {
id("com.android.library")
kotlin("multiplatform")
}
When doing so it's mandatory to include an AndroidManifest.xml, this can be placed in your src/androidMain/ and by including it in your script for the android build with:
The: build.gradle.kts
android {
sourceSets {
getByName("main") {
manifest.srcFile ("src/androidMain/AndroidManifest.xml")
}
}
}
The: AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.path.to.package.common"/>
NOTE: change the package path.

Related

Kotlin multiplatform project

I want to write a common library using Kotin multiplatform that can be used on android and on ios.
This library will have dependencies for each platform, for eg: on android I want to add jsoup as a dependency and on ios I want to add swiftsoup
For android adding java libraries as dependencies is rather easy, but for ios I could not find a way.
The question is: how can I add a swift library as a dependency to this project for ios?
or can somebody point me to a working project as an example? I could not find anything on the internet that could solve my issue.
build.gradle.kts :
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
val serializationVersion = "0.20.0"
val kotlinVersion = "1.3.72"
plugins {
kotlin("multiplatform") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
}
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
}
}
repositories {
jcenter()
mavenCentral()
}
kotlin {
//select iOS target platform depending on the Xcode environment variables
val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
::iosArm64
else
::iosX64
iOSTarget("ios") {
binaries {
framework {
baseName = "SharedCode"
}
}
}
jvm("android")
sourceSets["commonMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serializationVersion")
}
sourceSets["androidMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.jsoup:jsoup:1.13.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializationVersion")
}
sourceSets["iosMain"].dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serializationVersion")
}
}
val packForXcode by tasks.creating(Sync::class) {
group = "build"
//selecting the right configuration for the iOS framework depending on the Xcode environment variables
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val framework = kotlin.targets.getByName<KotlinNativeTarget>("ios").binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
val targetDir = File(buildDir, "xcode-frameworks")
from({ framework.outputDirectory })
into(targetDir)
doLast {
val gradlew = File(targetDir, "gradlew")
gradlew.writeText("#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$#\n")
gradlew.setExecutable(true)
}
}
tasks.getByName("build").dependsOn(packForXcode)
You can't use Swift dependencies in Kotlin unless they are compatible with Objective-C. If you want to talk to them directly, you'll need to point to them with cinterop. Alternatively, you can create interfaces in Kotlin, or take lambdas, that are implemented by Swift code, and avoid cinterop.
https://kotlinlang.org/docs/reference/native/objc_interop.html
We pass in a lot of implementations in one of our example apps: https://github.com/touchlab/DroidconKotlin/blob/master/iosApp/iosApp/app/AppDelegate.swift#L33

How to register generated resources in Android Gradle task?

I am trying to generate some Android resources in a Gradle task.
I've written a task which parses an input file, and writes out an XML file to a location under the app's build directory.
app/build.gradle
import groovy.xml.MarkupBuilder
task generateSomeAppResources {
ext.outputDir = new File(projectDir, "build/generated/res/values")
doFirst {
mkdir outputDir
new File(outputDir, "generated.xml").withWriter { writer ->
def destXml = new MarkupBuilder(new IndentPrinter(writer, " ", true, true))
destXml.setDoubleQuotes(true)
def destXmlMkp = destXml.getMkp()
destXmlMkp.xmlDeclaration(version: "1.0", encoding: "utf-8")
destXmlMkp.comment("Generated at ${new Date()}")
destXmlMkp.yield "\r\n"
destXml.resources() {
"string"("name": "generated_app_resource") {
destXmlMkp.yield("Some generated value for the app")
}
}
}
}
}
This works fine, and the generated output looks like I expect.
generated.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated at Wed Feb 12 12:46:12 GMT 2020 -->
<resources>
<string name="generated_app_resource">Some generated value for the app</string>
</resources>
I am struggling to get the Android build system to detect the generated file, though. Google's advice is
to write a task that outputs a generated resource directory structure with whatever you need, use BaseVariant.registerGeneratedResFolders()
But documentation on registerGeneratedResFolders() is non-existent. After much tedious searching I found some example usages in the Play Services Plugin source, for example, so I tried to add something along those lines.
app/build.gradle
android.applicationVariants.all { variant ->
def files = project.files(generateSomeAppResources.outputDir)
files.builtBy(generateSomeAppResources)
variant.preBuildProvider.configure { dependsOn(generateSomeAppResources) }
variant.mergeResourcesProvider.configure { dependsOn(generateSomeAppResources) }
variant.registerGeneratedResFolders(files)
}
But I'm missing something. The generated resource shows up purple in Android Studio, meaning that the IDE thinks it exists...
...but the code fails to compile with an Unresolved reference: generated_app_resource error.
I don't know what magic incantations are needed to make the Android build system pick up these resources. How do I get this to build?
To create resources, android requires
1) A resource directory above values folder then you can add desired resources as per your requirement
2) Instruct the build process to add the generated resources while building R.java
So first configure your build resource task like:
task generateSomeAppResources {
ext.outputDir = new File(projectDir, "build/generated/res/custom/values")
print("path is "+projectDir)
doFirst {
mkdir outputDir
new File(outputDir, "strings.xml").withWriter { writer ->
def destXml = new MarkupBuilder(new IndentPrinter(writer, " ", true, true))
destXml.setDoubleQuotes(true)
def destXmlMkp = destXml.getMkp()
destXmlMkp.xmlDeclaration(version: "1.0", encoding: "utf-8")
destXmlMkp.comment("Generated at ${new Date()}")
destXmlMkp.yield "\r\n"
destXml.resources() {
"string"("name": "generated_app_resource") {
destXmlMkp.yield("Some generated value for the app")
}
}
}
}
}
now add the path in the build process using sourceSets in build.gradle(app) like
android {
//....
sourceSets {
main {
res.srcDirs += [
'build/generated/res/custom',
]
}
}
}
Additionally, add the task in the current build process as
gradle.projectsEvaluated {
preBuild.dependsOn('generateSomeAppResources')
} // no need of `android.applicationVariants.all...`
Now sync the project and it will work as expected.
Result:
Example on how to generate resources using BaseVariant.registerGeneratedResFolders
def generateResourcesTask = tasks.register("generateResources", GenerateResourcesTask)
android.libraryVariants.all { variant ->
variant.registerGeneratedResFolders(
project.files(generateResourcesTask.map { it.outputDir })
)
}
Using tasks.register(...), project.files(...) and TaskProvider.map(...) utilizes task avoidance api, and automatically wires up tasks dependencies. registerGeneratedResFolders also automatically adds resources to sourceset, so that you can reference generated resources in your code.
abstract class GenerateResourcesTask : DefaultTask() {
// If you need inputs
// #get:InputFiles
// lateinit var inputs: FileCollection
#get:OutputDirectory
val outputDir = File(project.buildDir, "generated/res/regenerate/")
private val outputFile = File(outputDir, "values/missingRes.xml")
#TaskAction
fun action() {
val result = buildString {
appendln(
"""
<?xml version="1.0" encoding="utf-8"?>
<resources>
""".trimIndent()
)
appendln(""" <string name="test_string">Test string</string>""")
appendln("</resources>")
}
outputFile.parentFile.mkdirs()
outputFile.writeText(result)
}
}
You can define this task class in build.gradle file, or in buildSrc.
Using annotations like #OutputFile makes your task incremental.
For more info no what annotations do and how to use them check out gradle's documentation. You can also use Runtime API to create tasks with properties.

Kotlin / Native trouble importing android logs into android shared code module

I am trying to use Android logs in my shared code so wanted to make use of the 'expected/actual' functionality in order to make the android side use logs to be read in log cat. However I cannot get the android module(not app module) to import the android.util.Log.
I have seen this answer but it did not work for me. I cannot get the import to resolve.
I think I need to implement a specific dependency in order to have access to the import but I'm not sure what that is.
Here is my build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform")
}
kotlin {
//select iOS target platform depending on the Xcode environment variables
val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
::iosArm64
else
::iosX64
iOSTarget("ios") {
binaries {
framework {
baseName = "SharedCode"
}
}
}
jvm("android")
sourceSets["commonMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
implementation("io.ktor:ktor-client:1.0.0-beta-3")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.2")
// implementation ("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:0.14.0")
}
sourceSets["androidMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib") //Allows _androidMain to have java imports
implementation("io.ktor:ktor-client-android:1.0.0-beta-3")
api("org.jetbrains.kotlin:kotlin-stdlib:1.3.61")
api("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61")
}
}
val packForXcode by tasks.creating(Sync::class) {
val targetDir = File(buildDir, "xcode-frameworks")
/// selecting the right configuration for the iOS
/// framework depending on the environment
/// variables set by Xcode build
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val framework = kotlin.targets
.getByName<KotlinNativeTarget>("ios")
.binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
from({ framework.outputDirectory })
into(targetDir)
/// generate a helpful ./gradlew wrapper with embedded Java path
doLast {
val gradlew = File(targetDir, "gradlew")
gradlew.writeText("#!/bin/bash\n"
+ "export 'JAVA_HOME=${System.getProperty("java.home")}'\n"
+ "cd '${rootProject.rootDir}'\n"
+ "./gradlew \$#\n")
gradlew.setExecutable(true)
}
}
tasks.getByName("build").dependsOn(packForXcode)
Here you got JVM target with the name "android" instead of actually Android target. The same problem occurred in the linked question. Can you tell, what's going on when you use the script from the answer? It seems like that one should work correctly.
As described in the documentation, one has to use an Android-specific Gradle plugin to make the Android target available. If you want to see how it can be done, consider having a look at this sample.
I had the same problem. Try using android() instead of only the jvm("android").
Also I've added my dependencies to android with android.sourceSets.foreach{ _ ->
dependencies{ ... }
}
Just fixed same issue, finally used this tutorial https://medium.com/icerock/how-to-start-use-kotlin-multiplatform-for-mobile-development-1d3022742178
so my build.gradle looks like:
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.multiplatform'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 14
targetSdkVersion 29
}
}
kotlin {
targets {
android()
iosArm64()
iosX64()
}
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
}
}
androidMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
}
}
}

Using Android library in Kotlin multiplatform library

I have three targets commonMain/androidMain/iOSMain respectively. Because I need to access the assets in Android devices in androidMain module. I found I cannot use the Android API... The following is part of my build.gradle.kts:
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("com.android.application")
kotlin("multiplatform")
}
repositories {
google()
jcenter()
}
android {
compileSdkVersion(29)
buildToolsVersion("29.0.1")
defaultConfig {
minSdkVersion(19)
targetSdkVersion(29)
}
sourceSets {
getByName("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
java.srcDirs(file("src/androidMain/kotlin"))
res.srcDirs(file("src/androidMain/res"))
}
}
}
kotlin {
//select iOS target platform depending on the Xcode environment variables
val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
::iosArm64
else
::iosX64
iOSTarget("ios") {
binaries {
framework {
baseName = "Example"
}
}
}
jvm("android")
sourceSets["commonMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
}
sourceSets["androidMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
}
sourceSets["commonTest"].dependencies {
implementation ("org.jetbrains.kotlin:kotlin-test")
implementation ("org.jetbrains.kotlin:kotlin-test-junit")
}
}
How can I use Android library in androidMain? For example,
val inputStream = assets.open("Test.txt")
Make sure to create an AndroidManifest.xml with (library module) package name different than the (app module) package name underneath the SharedCode/src directory. And as Nagy Robi said use android() instead of jvm('android'). Please check the below references also:
Adding an Android Target to a Kotlin Multiplatform Project
Multiplatform sample
Try using android() instead of jvm('android') to load the target from the presets.

Gradle dynamic flavor

I would like to create dynamic flavors from the directory tree.
It works great!
But Android Studio uses gradle in its tmp file like:
/home/svirch_n/.IntelliJIdea14/system/compile-server
and my script doesn't work anymore because it uses relative paths like this:
Closure getFlavors = { rootDir, basePackage ->
def result = [:]
new File("$rootDir").eachDir() { dir ->
def name = dir.getName()
if ("$name" != "main")
result.put("$name", "$basePackage.$name")
}
return result
}
// This is an ugly closure.
// If I can get rid of this, my problem will be solved
Closure getSrcPath = {
if (System.getProperty("user.dir").split("/").last() == "app") {
return "src"
} else {
return "app/src"
}
}
android {
...
def myFlavors = getFlavors(getSrcPath(), "com.example.app")
productFlavors {
myFlavors.each { flavorName, flavorPackage ->
"$flavorName" {
applicationId "$flavorPackage"
}
}
}
}
Do you have an idea how to solve this?
Thanks in advance for your help
P.S: I want dynamic flavors cause my git project has public and private repositories and not everyone can have all the flavors but I want them to compile anyway.
Assuming I am in the subproject 'app', I can use:
project(":app").getProjectDir().getPath()

Categories

Resources