How to Package Dart Code for Use as a Native Library
The native projects pull in the required dependencies (including the Flutter/Dart code), expose fully native APIs (!!!), and package it all up for drop-in consumption by native projects.
I explained in a previous blog post how to call Dart code from native code in Android and iOS, leveraging Flutter’s Platform Channels. The approach is simple and works well when you are using your own libraries or have the Flutter SDK set up while also developing native code. But it starts to get tricky when you go to distribute your code for other developers to build on in their native apps.
Issue 1
Your library should be as lean as possible. When somebody decides to use a lib, they should review all of the code, including dependencies. Cascading dependencies through Flutter Module, Flutter Plugin, Flutter SDK, and Dart make your code unnecessarily complex to look through and understand.
Issue 2
The standard Flutter add-to-app build process requires all developers to install and configure the Flutter SDK in their development environments (and CI/CD, yuck). This is a steep barrier to entry, if not a deal-breaker altogether.
Issue 3
Building on Issue 2, one of the best parts of open-source software is opening the project to contributions from the community. If you’re creating a library for use by native Android or iOS developers, you need to present it in a native and intuitive structure. A developer that does not work with Flutter will have a steep learning curve before contributing.
The Fix
First, we split our project up into 3 distinct projects —Flutter, Android, and iOS. Instead of a single Flutter project building our iOS and Android dependencies, we’ll set up native iOS and Android projects. The native projects pull in the required dependencies (including the Flutter/Dart code), expose fully native APIs (!!!), and package it all up for drop-in consumption by native projects.
Flutter Project
We start unwrapping our code from the module/plugin mess. All we need is a single Flutter/Dart project. In this project we make three simple changes:
Add in your Platform Channel code that handles JUST the Dart side of the communication.
class TikiSdkFlutterPlatform { final methodChannel = const MethodChannel('YOUR_CHANNEL'); TikiSdkFlutterPlatform() { methodChannel.setMethodCallHandler(methodHandler); } Future<void> methodHandler(MethodCall call) async { ... } }
Add a main.dart file to lib. The Dart runtime looks for this as the Entrypoint to start your code. You can name it anything you want, but it requires additional configuration on the FlutterEngine side; it defaults to main.dart. The important part is that it needs to construct your Platform Channel from step 1.
void main() { WidgetsFlutterBinding.ensureInitialized(); TikiSdkFlutterPlatform(); ... }
Add to the bottom of your pubspec.yaml the module block —required by Flutter to build the AARs and XCFrameworks.
flutter: module: androidX: true androidPackage: com.your_company.artifact_id iosBundleIdentifier: com.your_company.artifact-id
Awesome! Now let's set up the build chain. There are a couple of nuances here. We want our iOS and Android projects to import the pre-compiled Flutter/Dart code without requiring the Flutter SDK. To do this, we’re going to need to host our AARs and XCFrameworks.
For AARs (Android), we like Github Packages (it’s free and colocated in the source repos), but if you already have Artifatory set up, use that. For XCFrameworks, we have fewer choices (maybe one day), so we simply attach them as artifacts to the Github Release.
This part is important. You’ll need to host ALL compiled dependencies for your projects. For example, our Flutter/Dart project utilizes the Path Provider lib. Flutter builds a different corresponding output for each of these dependencies. If you run flutter build aar and open build > host > outputs > repo, you’ll see them all. Your native projects are going to need all of these.
Now for one last quirk. If you’re using Flutter Integration tests, they’re packaged up in your outputs (which you do not want). You’ll want to comment out the dependency in your pubspec.yaml or remove it mid pipeline with a command like this.
perl -i -p0e 's/ integration_test:\n sdk: flutter//s' pubspec.yaml
So how do we publish them all? We recommend GitHub Actions (full source here).
iOS Release Pipeline
First, build the frameworks.
...
jobs:
publish:
...
- name: "Build Frameworks"
run: |
flutter build ios-framework --output=Frameworks --no-profile
Next, Zip up the required release frameworks (don’t forget the FlutterPluginRegistrant)
...
jobs:
publish:
...
- name: "Zip release frameworks"
run: |
cd Frameworks/Release
zip -r App_release.xcframework.zip App.xcframework
shasum -a 256 App_release.xcframework.zip | cut -f1 -d' '>> App.checksum.txt
zip -r Flutter_release.xcframework.zip Flutter.xcframework
shasum -a 256 Flutter_release.xcframework.zip | cut -f1 -d' '>> Flutter.checksum.txt
zip -r FlutterPluginRegistrant_release.xcframework.zip FlutterPluginRegistrant.xcframework
shasum -a 256 FlutterPluginRegistrant_release.xcframework.zip | cut -f1 -d' '>> FlutterPluginRegistrant.checksum.txt
...
Finally, publish!
...
jobs:
publish:
...
- name: Publish
uses: ncipollo/release-action@v1
with:
artifacts: "tiki-sdk-flutter/Frameworks/Release/*.zip,tiki-sdk-flutter/Frameworks/Release/*.checksum.txt,tiki-sdk-flutter/Frameworks/Debug/*.zip,tiki-sdk-flutter/Frameworks/Debug/*.checksum.txt"
replacesArtifacts: false
Android Release Pipeline
Since GitHub Packages supports Gradle Registries, we add a simple publish.gradle script to our project that loops over each project, publishing the artifacts.
apply plugin: 'maven-publish'
project.getGradle().projectsEvaluated {
allprojects {
publishing {
repositories {
maven {
url = uri("https://maven.pkg.github.com/ORG/REPO")
credentials {
username = System.getenv('GITHUB_USER')
password = System.getenv('GITHUB_TOKEN')
}
}
}
}
}
}
Publishing via GitHub Actions is very straight forward. All we need to do is append the publish.gradle script to the build.gradle file in the generated .android directory and then run flutter build aar --no-debug --no-profile --build-number <BUILD_NUMBER>
...
jobs:
publish:
...
- name: 'Flutter Build AAR'
run: |
flutter build aar
- name: 'Append publish gradle script'
run: |
cat publish.gradle >> .android/build.gradle
- name: 'Flutter Build AAR'
run: |
flutter build aar --no-debug --no-profile --build-number ${{ steps.vars.outputs.tag }}
env:
GITHUB_USER: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
That’s it for the Flutter side of things —find our specific implementation (100% open source) at github.com/tiki/tiki-sdk-flutter. For the corresponding iOS and Android sides, check out the platform-specific blogs:
Hi Ricardo, I just finished my own library based on the first blog that you wrote about using dart/flutter plugin in native android/ios and it works well. and then I want to call that library using some cloud repository like maven, so in order to do that, I need to follow the steps in this blog or not necessary? I wonder why in this blog and next blog you are not mention about the flutter plugin / module that we already created before, its a bit confusing for me, should i make a new module / new flutter project or how to use our previous plugin/module that we've made before, Thanks in advance.