6

I need to create an aar with all the libraries of my flutter project inside, I created a flutter module and now I've to create an sdk in android to be embedded in a client app for that it would be nice to have a single aar file. I tried Mobbeel fat AAR Gradle plugin but with no avail. I know I can create a maven repository but that is not the solution I'm looking for right now. enter image description here

my project build.gradle

buildscript {
    repositories {
        maven { url "https://plugins.gradle.org/m2/" }
        google()
        jcenter()

    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        classpath "com.mobbeel.plugin:fat-aar:2.0.1"
    }
}




allprojects {
    repositories {
        google()
        jcenter()
    }
}

and the app build.graddle

def flutterPluginVersion = 'managed'

apply plugin: 'com.android.library'
apply plugin: "com.mobbeel.plugin"

android {
    compileSdkVersion 28

    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }

    defaultConfig {
        minSdkVersion 21
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

buildDir = new File(rootProject.projectDir, "../build/host")



dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    api (project(':flutter'))


    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'androidx.annotation:annotation:1.1.0'
    implementation 'androidx.lifecycle:lifecycle-common:2.0.0'
}

aarPlugin {
    includeAllInnerDependencies false
    packagesToInclude = ["mobbeel"]
}

EDIT: I found a solution, but I'm not an android developer so a made some changes to the mobbeel plugin and add it to the build.gradle. After that I was able to add all the libraries to my aar by doing api project(":vibrate")

String archiveAarName

project.afterEvaluate {
    project.android.libraryVariants.all { variant ->

            variant.outputs.all {
                archiveAarName = outputFileName
            }

            print "afterEvaluate\n"

            def copyTask = createBundleDependenciesTask(variant)

            String rsDirPath = "${copyTask.temporaryDir.path}/rs/"
            String rsCompiledDirPath = "${copyTask.temporaryDir.path}/rs-compiled/"
            String sourceAarPath = "${copyTask.temporaryDir.path}/${variant.name}/"


            String taskNameCompileRs = "SDKcompileRs${variant.name.capitalize()}"
            String taskNameRsJa = "CreateRsJar${variant.name.capitalize()}"
            String taskNameCreateZip = "createZip${variant.name.capitalize()}"

            def compileRsTask = R2ClassTask(variant, rsDirPath, rsCompiledDirPath, taskNameCompileRs)
            def rsJarTask = bundleRJarTask(variant, rsCompiledDirPath, sourceAarPath, taskNameRsJa)
            def aarTask = bundleFinalAAR(variant, sourceAarPath, "finalname", taskNameCreateZip)


            def assembleTask = project.tasks.findByPath("assemble${variant.name.capitalize()}")

            assembleBundleDependenciesTask(variant).finalizedBy(assembleTask)
            assembleTask.finalizedBy(copyTask)
            copyTask.finalizedBy(compileRsTask)
            compileRsTask.finalizedBy(rsJarTask)
            rsJarTask.finalizedBy(aarTask)
        }
    }


Task assembleBundleDependenciesTask(def variant) {
    println "assembleBundleDependenciesTask -> ${variant.name}"

    return project.getTasks().create("hello_${variant}", {
        project.configurations.api.getDependencies().each { dependency ->

            if (dependency instanceof ProjectDependency) {

                Project dependencyProject = project.parent.findProject(dependency.name)

                String dependencyPath = "${dependencyProject.buildDir}"
                println "dependencyPath -> ${dependencyPath}"


                String variantName = "${variant.name}"

                def assembleTask = project.tasks.findByPath(":${dependency.name}:assemble${variant.name.capitalize()}")

                assembleTask.finalizedBy(copyTo( "${dependencyPath}/outputs/aar", variantName, dependency.name))
            }

            println ''
        }
    })

}

Task copyTo(String fromz, String variant, String dependency) {
    println "copyTo fromz -> $fromz "
    return project.task(type: Copy, "copyFile$dependency$variant") {
        from fromz
        into project.projectDir.path + "/build/outputs/aar/"
        include('*.aar')
        rename { String fileName ->
            fileName = "${dependency}-${variant}.aar"
        }
    }

}

Task createBundleDependenciesTask(def variant) {
    println "createBundleDependenciesTask -> ${variant.name}"

    String taskName = "copy${variant.name.capitalize()}Dependencies"
    return project.getTasks().create(taskName, CopyDependenciesTask.class, {
        it.includeInnerDependencies = true
        it.dependencies = project.configurations.api.getDependencies()
        it.variantName = variant.name
        it.gradleVersion = "3.2.1"
        it.buildAARDir = project.projectDir.path + "/build/outputs/aar/"
    })
}

Task R2ClassTask(def variant, String sourceDir, String destinationDir, String taskName) {
    print "R2ClassTask sourceDir -> $sourceDir to destDir -> $destinationDir"
    project.mkdir(destinationDir)

    def classpath

    classpath = project.files(project.projectDir.path +
            "/build/intermediates/javac/${variant.name}/compile${variant.name.capitalize()}JavaWithJavac/classes")



    return project.getTasks().create(taskName, JavaCompile.class, {
        it.source = sourceDir
        it.sourceCompatibility = project.android.compileOptions.sourceCompatibility
        it.targetCompatibility = project.android.compileOptions.targetCompatibility
        it.classpath = classpath
        it.destinationDir project.file(destinationDir)
    })
}

Task bundleRJarTask(def variant, String fromDir, String aarPath, String taskName) {
    print "bundleRJarTask\n"

    return project.getTasks().create(taskName, Jar.class, {
        it.from fromDir
        it.archiveName = "r-classes.jar"
        it.destinationDir project.file("${aarPath}/libs")
    })
}

Task bundleFinalAAR(def variant, String fromPath, name, String taskName) {
    print "bundleFinalAAR -> from ${fromPath} to > " + project.file(project.projectDir.path + "/build/outputs/aar/") + "\n"

    return project.getTasks().create(taskName, Zip.class, {
        it.from fromPath
        it.include "**"
        it.archiveName = "${name}-${variant.name}.aar"
        it.destinationDir(project.file(project.projectDir.path + "/build/outputs/aar/"))
    })
}

import groovy.xml.XmlUtil

class CopyDependenciesTask extends DefaultTask {

    Boolean includeInnerDependencies
    DependencySet dependencies
    String variantName
    String gradleVersion
    String[] packagesToInclude = [""]
    String buildAARDir

    @TaskAction
    def executeTask() {
        if (temporaryDir.exists()) {
            temporaryDir.deleteDir()
        }
        temporaryDir.mkdir()

        copyProjectBundles()
        analyzeDependencies()
    }


    def copyProjectBundles() {
        println "copyProjectBundles"

        if (gradleVersion.contains("3.2")) { // Version 3.4.x
            println "packaged-classes -> ${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/packaged-classes/"
            project.copy {
                from "${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/packaged-classes/"
                include "${variantName}/**"
                into temporaryDir.path
            }


            project.copy {
                from("${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/res/symbol-table-with-package/${variantName}") {
                    include "package-aware-r.txt"
                    rename '(.*)', 'R.txt'
                }

                from("${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/aapt_friendly_merged_manifests/" +
                        "${variantName}/process${variantName.capitalize()}Manifest/aapt/") {
                    include "AndroidManifest.xml"
                }

                into "${temporaryDir.path}/${variantName}"
            }

            println " check this -> ${temporaryDir.path}/${variantName}/R.txt"

            processRsAwareFile(new File("${temporaryDir.path}/${variantName}/R.txt"))

            project.copy {
                from "${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/packaged_res/${variantName}"
                include "**"
                into "${temporaryDir.path}/${variantName}/res"
            }

            project.copy {
                from "${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/library_assets/${variantName}/packageDebugAssets/out/"
                include "**"
                into "${temporaryDir.path}/${variantName}/assets"
            }
        }  else { // Version 3.0.x
            project.copy {
                from "${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/bundles/"
                from "${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/manifests/full/"
                include "${variantName}/**"
                exclude "**/output.json"
                into temporaryDir.path
            }
        }
    }

    def analyzeDependencies() {
        print "analyzeDependencies\n"
        dependencies.each { dependency ->
            def dependencyPath
            def archiveName
            print "dependency -> " + dependency
            if (dependency instanceof ProjectDependency) {
                print " instanceof -> ProjectDependency\n"
                String group = dependency.group
                Project dependencyProject

                dependencyProject = project.parent.findProject(dependency.name)


                println "dependencyProject -> ${dependencyProject}"


                if (dependencyProject.plugins.hasPlugin('java-library')) {
                    println "Internal java dependency detected -> " + dependency.name

                    archiveName = dependencyProject.jar.archiveName

                    dependencyPath = "${dependencyProject.buildDir}/libs/"
                } else {
                    println "Internal android dependency detected -> " + dependency.name

                    dependencyProject.android.libraryVariants.all {
                        if (it.name == variantName) {
                            it.outputs.all { archiveName = outputFileName }
                        }
                    }

                    dependencyPath = buildAARDir
                }

                processDependency(dependency, archiveName, dependencyPath)
            } else if (dependency instanceof ExternalModuleDependency) {
                println "External dependency detected -> " + dependency.group + ":" + dependency.name + ":" + dependency.version
                dependencyPath = project.gradle.getGradleUserHomeDir().path + "/caches/modules-2/files-2.1/"
                dependencyPath += dependency.group + "/" + dependency.name + "/" + dependency.version + "/"

                processDependency(dependency, archiveName, dependencyPath)
            } else {
                println "Not recognize type of dependency for " + dependency
                println()
            }
        }
    }

    /**
     * In this case dependency is outside from workspace, download from maven repository if file is
     * a jar directly move to lib/ folder and analyze pom file for detect another transitive dependency
     * @param dependency
     * @return
     */
    def processDependency(Dependency dependency, String archiveName, String dependencyPath) {
        println "processDependency -> ${archiveName} in ${dependencyPath}"
        project.fileTree(dependencyPath).getFiles().each { file ->
            println "processDependency file.name  -> ${file.name} "
            if (file.name.endsWith(".pom")) {
                println "POM: " + file.name
                processPomFile(file.path)
            } else {
                if (archiveName == null || file.name == archiveName) {
                    println "Artifact: " + file.name
                    if (file.name.endsWith(".aar")) {
                        processZipFile(file, dependency)
                    } else if (file.name.endsWith(".jar")) {
                        if (!file.name.contains("sources")) {
                            copyArtifactFrom(file.path)
                        } else {
                            println "   |--> Exclude for source jar"
                        }
                    }
                }
            }
        }
        println()
    }

    def processZipFile(File aarFile, Dependency dependency) {
        println "processZipFile"

        String tempDirPath = "${temporaryDir.path}/${dependency.name}_zip"

        println "tempDirPath -> ${tempDirPath}"

        project.copy {
            from project.zipTree(aarFile.path)
            include "**/*"
            into tempDirPath
        }

        File tempFolder = new File(tempDirPath)

        println "temporaryDir -> ${temporaryDir.path}/${variantName}/"

        project.copy {
            from "${tempFolder.path}"
            include "classes.jar"
            into "${temporaryDir.path}/${variantName}/libs"
            def jarName = getJarNameFromDependency(dependency)
            rename "classes.jar", jarName
        }

        project.copy {
            from "${tempFolder.path}/libs"
            include "**/*.jar"
            into "${temporaryDir.path}/${variantName}/libs"
        }

        project.copy {
            from "${tempFolder.path}/jni"
            include "**/*.so"
            into "${temporaryDir.path}/${variantName}/jni"
        }

        project.copy {
            from "${tempFolder.path}/assets"
            include "**/*"
            into "${temporaryDir.path}/${variantName}/assets"
        }

        project.copy {
            from "${tempFolder.path}/res"
            include "**/*"
            exclude "values/**"
            into "${temporaryDir.path}/${variantName}/res"
        }

        processValuesResource(tempFolder.path)
        processRsFile(tempFolder)

        println "tempFolder.deleteDir()"
        tempFolder.deleteDir()
    }

    def getJarNameFromDependency(Dependency dependency) {
        def jarName = ""
        if (null != dependency.group) {
            jarName += dependency.group.toLowerCase() + "-"
        }
        jarName += dependency.name.toLowerCase()
        if(null != dependency.version && !dependency.version.equalsIgnoreCase('unspecified')) {
            jarName += "-" + dependency.version
        }
        jarName += ".jar"

        return jarName
    }

    def processRsAwareFile(File resAwareFile) {
        println "processRsAwareFile"
        RandomAccessFile raf = new RandomAccessFile(resAwareFile, "rw")

        long writePosition = raf.getFilePointer()
        raf.readLine() // Move pointer to second line of file
        long readPosition = raf.getFilePointer()

        byte[] buffer = new byte[1024]
        int bytesInBuffer

        while (-1 != (bytesInBuffer = raf.read(buffer))) {

            raf.seek(writePosition)

            raf.write(buffer, 0, bytesInBuffer)
            readPosition += bytesInBuffer
            writePosition += bytesInBuffer

            raf.seek(readPosition)
        }
        raf.setLength(writePosition)

        raf.seek(0)

        if (gradleVersion.contains("3.2")) {
            String filePath = "${project.projectDir.parentFile.parent}/finalProjname/app/build/intermediates/symbols/${variantName}/R.txt"
            Scanner resourcesOriginal = new Scanner(new File(filePath))

            raf.seek(0) // Move pointer to first line

            String line
            int offset = 0
            while (resourcesOriginal.hasNextLine()) {
                boolean match = false
                line = resourcesOriginal.nextLine()
                println line

                line += "\n"

                byte[] data = line.getBytes()

                raf.seek(offset)
                raf.write(data, 0, data.length)
                offset += data.length

                raf.seek(offset + 1)

            }
        }

        raf.close()
    }

    def processRsFile(File tempFolder) {
        println "processRsFile"

        def mainManifestFile = project.android.sourceSets.main.manifest.srcFile
        def libPackageName = ""

        if (mainManifestFile.exists()) {
            println "processRsFile -> mainManifestFile.exists()"
            libPackageName = new XmlParser().parse(mainManifestFile).@package
        }

        def manifestFile = new File("$tempFolder/AndroidManifest.xml")
        if (manifestFile.exists()) {
            println "processRsFile -> manifestFile.exists()"
            def aarManifest = new XmlParser().parse(manifestFile)
            def aarPackageName = aarManifest.@package

            String packagePath = aarPackageName.replace('.', '/')

            // Generate the R.java file and map to current project's R.java
            // This will recreate the class file
            def rTxt = new File("$tempFolder/R.txt")
            def rMap = new ConfigObject()

            if (rTxt.exists()) {
                println "processRsFile -> rTxt.exists()"
                rTxt.eachLine { line ->
                    //noinspection GroovyUnusedAssignment
                    def (type, subclass, name, value) = line.tokenize(' ')
                    rMap[subclass].putAt(name, type)
                }
            }

            def sb = "package $aarPackageName;" << '\n' << '\n'
            sb << 'public final class R {' << '\n'

            rMap.each { subclass, values ->
                sb << "  public static final class $subclass {" << '\n'
                values.each { name, type ->
                    sb << "    public static $type $name = com.company.native_sdk.R.${subclass}.${name};" << '\n'
                }
                sb << "    }" << '\n'
            }

            sb << '}' << '\n'

            new File("${temporaryDir.path}/rs/$packagePath").mkdirs()
            FileOutputStream outputStream = new FileOutputStream("${temporaryDir.path}/rs/$packagePath/R.java")
            println "R file path -> ${temporaryDir.path}/rs/$packagePath/R.java"
            outputStream.write(sb.toString().getBytes())
            outputStream.close()
        }
    }

    def processValuesResource(String tempFolder) {
        println "processValuesResource"

        File valuesSourceFile = new File("${tempFolder}/res/values/values.xml")
        File valuesDestFile = new File("${temporaryDir.path}/${variantName}/res/values/values.xml")

        if (valuesSourceFile.exists()) {
            println "processValuesResource -> valuesSourceFile.exists"
            if (!valuesDestFile.exists()) {
                println "processValuesResource -> !valuesDestFile.exists"
                project.copy {
                    from "${tempFolder}/res"
                    include "values/*"
                    into "${temporaryDir.path}/${variantName}/res"
                }
            } else {
                println "processValuesResource -> valuesDestFile.exists"
                def valuesSource = new XmlSlurper().parse(valuesSourceFile)
                def valuesDest = new XmlSlurper().parse(valuesDestFile)

                valuesSource.children().each {
                    valuesDest.appendNode(it)
                }

                FileOutputStream fileOutputStream = new FileOutputStream(valuesDestFile, false)
                byte[] myBytes = XmlUtil.serialize(valuesDest).getBytes("UTF-8")
                fileOutputStream.write(myBytes)
                fileOutputStream.close()
            }
        } else {
            println "processValuesResource -> !valuesSourceFile.exists"
        }
    }

    def copyArtifactFrom(String path) {
        project.copy {
            includeEmptyDirs false
            from path
            include "**/*.jar"
            into "${temporaryDir.path}/${variantName}/libs"
            rename '(.*)', '$1'.toLowerCase()
        }
    }

    def processPomFile(String pomPath) {
        def pom = new XmlSlurper().parse(new File(pomPath))
        pom.dependencies.children().each {
            def subJarLocation = project.gradle.getGradleUserHomeDir().path + "/caches/modules-2/files-2.1/"
            if (!it.scope.text().equals("test") && !it.scope.text().equals("provided")) {
                String version = it.version.text()
                if (version.startsWith("\${") && version.endsWith("}")) {
                    pom.properties.children().each {
                        if (version.contains(it.name())) {
                            version = it.text()
                        }
                    }
                }

                println "   |--> Inner dependency: " +  it.groupId.text() + ":" + it.artifactId.text() + ":" + version

                if (includeInnerDependencies || it.groupId.text() in packagesToInclude) {
                    subJarLocation += it.groupId.text() + "/" + it.artifactId.text() + "/" + version + "/"
                    project.fileTree(subJarLocation).getFiles().each { file ->
                        if (file.name.endsWith(".pom")) {
                            println "   /--> " + file.name
                            processPomFile(file.path)
                        } else {
                            if (!file.name.contains("sources") && !file.name.contains("javadoc")) {
                                copyArtifactFrom(file.path)
                            }
                        }
                    }
                } else {
                    println "        (Exclude inner dependency)"
                }
            }
        }
    }
}
2
  • Hi, Manuel. I also want to export the flutter module as an aar file. Did you get any solution? Commented Jul 1, 2019 at 7:25
  • @NiravTukadiya take a look at my new edit. Hope that it helps you. Commented Jul 15, 2019 at 14:23

2 Answers 2

7
  1. Add Android native library into your project in Android Studio: File -> New -> New module -> Android Library.

  2. After that add this plugin https://github.com/kezong/fat-aar-android into the project and replace 'implementation' by 'embed' keyword. Then your project structure will look like: enter image description here

  3. In flutter_library directory run command flutter build aar -v. Note: flutter_library contains Flutter related files, e.g lib/, .android, .ios, pubspec.yaml, etc

  4. In root project directory run ./gradlew assemble

  5. aar will be located in library/build/outputs/aar

See my example: https://github.com/askarsyzdykov/native_flutter_lib

Sign up to request clarification or add additional context in comments.

Comments

3

The aar file doesn't contain the transitive dependencies and doesn't have a pom file which describes the dependencies used by the library.

It means that, if you are importing a aar file using a flatDir repo you have to specify the dependencies also in your project.

I know that it is not the solution you are looking for but you should use a maven repository to solve this issue. In this case, gradle downloads the dependencies using the pom file which will contains the dependencies list.

1 Comment

So if I have a maven solution do I've to have all flutter plugins in my maven repository as well?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.