TIKI

Share this post
How to Package Dart for Use as a Native Android Library
blog.mytiki.com
Developers

How to Package Dart for Use as a Native Android Library

We cover creating a fat-aar, adding-in dynamic link libraries (dlls), and publishing to a Maven Repository. The goal is to create a dependency an Android developer can drop into their existing project

Mike Audi
Dec 9, 2022
Share this post
How to Package Dart for Use as a Native Android Library
blog.mytiki.com

This post builds on the previous post about how to package a Dart library using Flutter. In this blog we cover creating a fat-aar, adding-in dynamic link libraries (dlls), and publishing to a Maven Repository. The goal is to create a dependency an Android developer can drop into their existing project via a single-line standard implementation call.

implementation 'com.mytiki:tiki_sdk_android:0.0.3'

Android Project

We start by creating a new empty Android project. Inside the project’s app/src/main/kotlin we add our Android Flutter Channel code to map our Dart requests and responses to native APIs.

FlutterChannel.kt
...

import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.util.*

class FlutterChannel : FlutterPlugin, MethodCallHandler  {
    private lateinit var channel: MethodChannel

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "YOUR_CHANNEL")
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        ...
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }

    fun invokeMethod(method: String, arguments: MutableMap<String,Any?> {
        channel.invokeMethod(method, arguments)
    }
}

In our native SDK API, we initialize the Flutter Channel and wire up our methods.

sdk.kt
class Sdk(context: Context) {
    private var flutterChannel: FlutterChannel

    init {
        val loader = FlutterLoader()
        loader.startInitialization(context)
        loader.ensureInitializationComplete(context, null)
        val flutterEngine = FlutterEngine(context)
        flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        flutterChannel = FlutterChannel()
        flutterEngine.plugins.add(flutterChannel)
    }

    private fun hello() {
        tikiSdkFlutterChannel.invokeMethod(
            "hello", mutableMapOf(
                "message" to message
            )
        )
    }
}

We use callbacks with requestIds to map responses to API calls. There are a number of ways to implement this from within onMethodCall —but if you want to take inspiration from our design, the source code can be seen here.

Fat AAR

Next, you’re going to need a Fat AAR. The standard AAR generated by ./gradlew assemble only includes your Android source, but for it to run as a single import, we also need to include our Dart code and any Flutter plugins you may have added along the way.

We’re going to use the Gradle plugin fat-arr from kenzong, to add in our hosted dependencies we created in the previous blog. To do this we first add (or amend) a buildscript block to the top of our build.gradle file inside the app directory.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.github.kezong:fat-aar:1.3.8'
    }
}

Now, in our dependency block, we can call embed instead of implementation for the dependencies we want to include in our AAR. Don’t forget to apply the plugin before the block. Feel free to use ours as an example.

apply plugin: 'com.kezong.fat-aar'
dependencies {
    ...

    debugEmbed "com.your_company.flutter_sdk:flutter_debug:VERSION"
    releaseEmbed "ccom.your_company.flutter_sdk:flutter_release:VERSION"
}

Lastly, we need to pull in the Flutter embeddings; without it, your Platform Channel won’t be able to resolve its dependencies. First, we add to the same dependency block:

    debugEmbed "io.flutter:flutter_embedding_debug:ENGINE_VERSION"
    releaseEmbed "io.flutter:flutter_embedding_release:ENGINE_VERSION"

Where ENGINE_VERSION is the Flutter Engine version, you intend to use. For example, when this blog was written, we were using 1.0.0-857bd6b74c5eb56151bfafe91e7fa6a82b6fee25. You can find the corresponding engine version on Flutter’s Github.

Then in our settings.gradle file (project root), we need to tell Gradle where to fetch the embedding AARs.

dependencyResolutionManagement {
    ...
        maven {
            url 'https://storage.googleapis.com/download.flutter.io'
        }
    }
}

AAR, done. Now for the DLLs.

Adding DDLs

AARs can optionally include DLLs via the jniLibs folder; this is great because the Flutter Engine is pre-compiled per-platform (arm64, armeabi, etc.). Similarly, it may not be the only DLL your project relies on. For example, we use SQLite in our project, and so we need to include its corresponding DLL too.

You can manually fetch the libflutter.so binaries from the same URL as the embedding libraries and place them in the standard jniLibs folder structure like this:

app
 src
  main
   jniLibs
    arm64-v8a
     libflutter.so
    armeabi-v7a
     libflutter.so
    x86_64
     libflutter.so
  debub
   jniLibs
    arm64-v8a
     libflutter.so
    armeabi-v7a
     libflutter.so
    x86
     libflutter.so
    x86_64
     libflutter.so

Or, fetch and load them automatically via a Gradle script by adding a couple more lines to the bottom of your build.gradle file.

def groovyShell = new GroovyShell()
def addDll = groovyShell.parse(new File("$rootDir/app/add-dll.gradle"))

addDll.clean(rootDir.toString())
addDll.flutter(rootDir.toString(), flutterEngine)

You can find our add-dll.gradle script here. It exposes functions to fetch and load just about any DLL, plus a couple nifty flutter and SQLite specific functions. Some folks choose to commit their jniLibs; we don’t since we fetch them automatically on Gradle Syncs.

Publish to Maven

Our project is open source, so for this example, we’ll use the Maven Central repository provided by Sonatype (follow the instructions here to set up your own account) to host our final AAR. If you don’t want to release your AAR publicly, check out Artifactory or GitHub Packages.

First, we add signing and publishing to the end of our app’s build.gradle file.

...

apply plugin: 'maven-publish'
apply plugin: 'signing'

signing {
    def signingKey = System.getenv("PGP_PRIVATE_KEY")
    def signingPassword = System.getenv("PGP_PASSPHRASE")
    useInMemoryPgpKeys(signingKey, signingPassword)
    sign publishing.publications
}

afterEvaluate {
    publishing {
        publications {
            release(MavenPublication) {
                from components.release

                groupId = 'com.your_company'
                artifactId = 'sdk_android'
                version = android.getDefaultConfig().getVersionName()
            }
        }
        repositories {
            maven {
                name = "OSSRH"
                url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
                credentials {
                    username = System.getenv('OSSRH_USER')
                    password = System.getenv('OSSRH_TOKEN')
                }
            }
        }
    }
}

We’re using environment variables to pass our tokens and signing keys via Github Secrets in Actions, but if you’re going to release from a local machine, you can use findProperty(“NAME“) and keep your secrets in your gradle.properties file (your .gradle root folder, not your projects).

The Sonatype instructions show you how to use GPG to create a PGP key-pair and upload (required) the public key to the Ubuntu Keyserver. Once you have the key pair generated, you simply copy your private key and upload it to GitHub Secrets.

gpg --armor --export-secret-key USERNAME | pbcopy

We add to GitHub Secrets the private key passphrase and sonatype OSSRH token username and password (don’t use your plaintext password in build pipelines). Last but not least, we add a Github Action to publish.

...

jobs:
  publish:
    ...
    steps:
      ...

      - name: Validate Gradle Wrapper
        uses: gradle/wrapper-validation-action@v1

      - name: Assemble AAR
        uses: gradle/gradle-build-action@v2
        with:
          arguments: assemble
        env:
          GITHUB_USER: ${{ github.actor }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: "Publish AAR"
        uses: gradle/gradle-build-action@v2
        with:
          arguments: publish
        env:
          OSSRH_USER: ${{ secrets.OSSRH_USER }}
          OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }}
          GITHUB_USER: ${{ github.actor }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
          PGP_PRIVATE_KEY: ${{ secrets.PGP_PRIVATE_KEY }}

Done! Now, all a developer has to do to add your library to their project is:

implementation 'com.your_company:sdk_android:VERSION'

Yes, it’s a bit of a pain to get the entire build chain set up, but if you want to build killer cross-platform SDKs, Dart + Flutter makes for a powerful combo. Key business logic can live inside a single project, dramatically reducing overhead. Platform-specific projects handle wrapping APIs for native calls, dependency management, and automated deployment.

To see our project end-to-end, check out the following: tiki-sdk-dart, tiki-sdk-flutter, tiki-sdk-android

Help a dev get some sleep.

Share

Share this post
How to Package Dart for Use as a Native Android Library
blog.mytiki.com
Previous
Next
Comments
TopNewCommunity

No posts

Ready for more?

© 2023 TIKI Inc.
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing