Create an aar with all flutter libraries and dependencies inside

3,166

Solution 1

  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

Solution 2

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.

Share:
3,166
Manuel Peixoto
Author by

Manuel Peixoto

Updated on December 12, 2022

Comments

  • Manuel Peixoto
    Manuel Peixoto over 1 year

    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)"
                    }
                }
            }
        }
    }
    
    • Nirav Tukadiya
      Nirav Tukadiya almost 5 years
      Hi, Manuel. I also want to export the flutter module as an aar file. Did you get any solution?
    • Manuel Peixoto
      Manuel Peixoto almost 5 years
      @NiravTukadiya take a look at my new edit. Hope that it helps you.
  • Manuel Peixoto
    Manuel Peixoto almost 5 years
    So if I have a maven solution do I've to have all flutter plugins in my maven repository as well?