Publishing your open-source Flutter app on F-Droid is an incredible way to reach a privacy-conscious user base. However, bridging the gap between a local Flutter project, a GitHub Actions pipeline, and F-Droid's strict build servers can be a daunting process.
In this guide, I will walk you through the exact steps to get your Flutter app packaged, signed, and published on F-Droid.
(Note: This guide assumes you are hosting your code and managing releases via GitHub. Other platforms will have a similar setup, but the CI/CD steps may vary).
Part 1: Preparing Your Local Project
Before we automate anything, we need to get your Android project ready for F-Droid's strict requirements.
1. Remove "Anti-Features"
F-Droid has a strict policy against proprietary software. Make sure you are not using any closed-source Google Play Services, analytics trackers, or proprietary crash reporters. If your app requires them, you must declare them. Read up on F-Droid's Anti-Features docs before proceeding.
2. Update build.gradle.kts
We need to modify the android/app/build.gradle.kts file to accomplish three things: dynamically load our signing configuration, strip Google's proprietary dependency block, and split our APKs by architecture (which F-Droid strongly prefers).
Update your build.gradle.kts to look like this:
// ... inside the android block
signingConfigs {
if (System.getenv("CI") == "true") {
create("release") {
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
storeFile = System.getenv("KEYSTORE_PATH")?.let { file(it) }
storePassword = System.getenv("KEYSTORE_PASSWORD")
}
}
}
buildTypes {
release {
signingConfig = if (System.getenv("CI") == "true") {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
// Enable code shrinking to reduce APK size
isMinifyEnabled = true
isShrinkResources = true
}
}
// Strip Google proprietary block from APK
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
}
// --- F-Droid ABI Split Version Code Generation ---
val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86_64" to 3)
android.applicationVariants.configureEach {
val variant = this
variant.outputs.forEach { output ->
val abiVersionCode = abiCodes[output.filters.find { it.filterType == "ABI" }?.identifier]
if (abiVersionCode != null) {
(output as ApkVariantOutputImpl).versionCodeOverride = variant.versionCode * 10 + abiVersionCode
}
}
}
3. Generate Your Keystore
To sign your app, generate a local keystore by running this in your terminal:
keytool -genkey -v -keystore <key-file-name>.jks -keyalg RSA -keysize 2048 -validity 10000 -alias <alias>
⚠️ **Crucial:* Remember your password and back up this .jks file! If you lose it, you will never be able to update your app again.*
Next, convert this file into a Base64 string so we can safely pass it to GitHub Actions:
base64 -i <key-file-name>.jks > keystore_b64.txt
Part 2: Automating with GitHub Actions
Now we configure GitHub to build our APKs exactly the way F-Droid expects them.
1. Set up Repository Secrets
Navigate to your GitHub repository Settings > Secrets and variables > Actions > Repository secrets and add these four keys:
-
KEYSTORE_BASE64(Paste the contents ofkeystore_b64.txthere) KEYSTORE_PASSWORDKEY_ALIASKEY_PASSWORD
2. The Release Action (release.yml)
Create a GitHub Action to build and upload your APKs. To ensure maximum compatibility with F-Droid's build servers, you must hardcode your Flutter version and strip the C++ build IDs from your dependencies.
Here is an extract of the necessary build job. (You can view my complete release.yml here).
jobs:
build_apk:
name: Build Android APKs (Split)
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '21'
# IMPORTANT: Hardcode the flutter version!
- name: Setup Flutter
uses: subosito/flutter-action@v2
id: flutter-action
with:
channel: 'stable'
flutter-version: '3.41.9'
- name: Install dependencies
run: flutter pub get
# F-DROID COMPATIBILITY: Strip build IDs from C++ modules
- name: Remove build id
run: sed -i -e 's/-Wl,/-Wl,--build-id=none,/' ${{ steps.flutter-action.outputs.PUB-CACHE-PATH }}/hosted/pub.dev/jni-*/src/CMakeLists.txt
- name: Decode Keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/release-keystore.jks
- name: Build Split APKs
env:
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_PATH: release-keystore.jks
run: flutter build apk --release --split-per-abi --split-debug-info=build/symbols
# ... (Add steps here to rename and upload your artifacts to the GitHub Release)
Run this action to build your APKs, and ensure they are attached to a tagged GitHub Release (e.g., v1.2.3).
Part 3: The F-Droid Metadata
F-Droid builds your app from source and compares it against the APKs you uploaded to GitHub.
First, fetch the exact Git commit hash of your new release tag locally, you'll need this in a moment:
git fetch && git pull
git rev-parse v1.2.3
Writing the Configuration File
- Fork the F-Droid Data Repository on GitLab.
- In the
metadata/folder of your fork, create a new file named after your App ID (e.g.,io.github.benji377.timety.yml). Do not use a random name!
Paste the following template, replacing the placeholder data with your own. Note: The complex sudo and mv commands in the build blocks are a necessary trick to align F-Droid's compiler paths with GitHub Actions, preventing signature mismatches!
Categories:
- Habit Tracker
- Task
- Time
License: GPL-3.0-only
AuthorName: YourName
AuthorWebSite: https://github.com/YourName
SourceCode: https://github.com/YourName/YourApp
IssueTracker: https://github.com/YourName/YourApp/issues
Donate: https://github.com/sponsors/YourName
AutoName: YourApp
RepoType: git
Repo: https://github.com/YourName/YourApp.git
Builds:
- versionName: 1.2.3
versionCode: 121
commit: <YOUR_GIT_HASH_HERE>
sudo:
- mkdir -p /home/runner/work/<YourApp>
- chown -R vagrant /home/runner
output: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
binary:
https://github.com/YourName/YourApp/releases/download/v%v/yourapp-v%v-armeabi-v7a.apk
srclibs:
- flutter@stable
rm:
- web
prebuild:
- export repo=/home/runner/work/YourApp/YourApp
- cd ..
- mv com.your.package $repo
- pushd $repo
- flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" .github/workflows/release.yml)
- '[[ $flutterVersion ]]'
- git -C $$flutter$$ checkout -f $flutterVersion
- export PUB_CACHE=$(pwd)/.pub-cache
- $$flutter$$/bin/flutter config --no-analytics
- $$flutter$$/bin/flutter pub get
- sed -i -e 's/-Wl,/-Wl,--build-id=none,/' $PUB_CACHE/hosted/pub.dev/jni-*/src/CMakeLists.txt
- popd
- mv $repo com.your.package
scandelete:
- .pub-cache
build:
- export repo=/home/runner/work/YourApp/YourApp
- cd ..
- mv com.your.package $repo
- pushd $repo
- export PUB_CACHE=$(pwd)/.pub-cache
- $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm" --split-debug-info=build/symbols
- popd
- mv $repo com.your.package
# NOTE: Duplicate the Build block above for versionCode 122 (arm64-v8a)
# and versionCode 123 (x86_64)
AllowedAPKSigningKeys: <YOUR_FINGERPRINT_HERE>
AutoUpdateMode: Version
UpdateCheckMode: Tags
VercodeOperation:
- '%c * 10 + 1'
- '%c * 10 + 2'
- '%c * 10 + 3'
UpdateCheckData: pubspec.yaml|version:\s.+\+(\d+)|.|version:\s(.+)\+
CurrentVersion: 1.2.3
CurrentVersionCode: 123
Retrieving the Keystore Fingerprint
At the bottom of the YAML file, you'll see AllowedAPKSigningKeys. This tells F-Droid to trust your specific keystore. Extract it by running this against one of the APKs you downloaded from GitHub:
keytool -printcert -jarfile app-release.apk | sed -n 's/[[:space:]]*SHA256: //p' | tr -d ':' | tr '[:upper:]' '[:lower:]'
Part 4: Submit and Troubleshooting
Create a Merge Request (MR) in the F-Droid GitLab repository to merge your new .yml file. This triggers their CI pipeline.
Pro-Tips for a smooth Merge Request:
- Strict YAML: The pipeline's linter is unforgiving. If it fails for formatting, download the "auto-corrected" YAML file from the pipeline artifacts and overwrite yours.
-
The
rmblock: In theBuildssection, only include platforms your project actually has in thermarray. If you don't have anios/folder, don't tell F-Droid to delete it, or the build will fail. -
Bumping Versions: If your build fails and you need to push a fix to your app's code, you must create a brand new GitHub release tag, bump the version code, and update the
commithash in your F-Droid YAML.
If everything passes, an F-Droid maintainer will review your MR, merge it, and your Flutter app will officially be available on F-Droid!
Want to see a working example? Check out the source code for my offline-first productivity app, Timety on GitHub, and its F-Droid Metadata File.
Top comments (0)