linkAndroid function
void
linkAndroid(})
Configures android/build.gradle (or .kts) so the generated Kotlin bridge
files in lib/src/generated/kotlin/ are compiled as part of the Android build.
Without the kotlin.srcDirs entry, all .bridge.g.kt files are generated but
never compiled — causing "Unresolved reference: XxxJniBridge" errors at build time.
Implementation
void linkAndroid(
String pluginName,
List<String> moduleLibs, {
String baseDir = '.',
List<ModuleInfo>? moduleInfos,
}) {
File? buildGradle;
for (final candidate in [
File(p.join(baseDir, 'android', 'build.gradle')),
File(p.join(baseDir, 'android', 'build.gradle.kts')),
]) {
if (candidate.existsSync()) {
buildGradle = candidate;
break;
}
}
if (buildGradle == null) return;
var content = buildGradle.readAsStringSync();
bool modified = false;
final isKts = buildGradle.path.endsWith('.kts');
// 0. Upgrade old-style `apply plugin: "kotlin-android"` to modern plugins{} DSL.
// The legacy `buildscript {}` + `apply plugin` approach fails in modern AGP
// because `kotlin-android` alias is not resolvable without the classpath in
// the consuming app's settings.gradle. Modern Flutter apps use `plugins {}`.
if (content.contains('apply plugin: "kotlin-android"') ||
content.contains("apply plugin: 'kotlin-android'")) {
// Remove the entire buildscript block if present.
content = content.replaceAll(
RegExp(r'\bbuildscript\s*\{[^}]*\{[^}]*\}[^}]*\}\s*\n?', dotAll: true),
'',
);
// Remove rootProject.allprojects block.
content = content.replaceAll(
RegExp(r'\brootProject\.allprojects\s*\{[^}]*\}\s*\n?', dotAll: true),
'',
);
// Replace apply plugin lines with plugins{} block.
content = content.replaceAll(
RegExp(r"apply plugin:\s*'com\.android\.library'\s*\n?"),
'',
);
content = content.replaceAll(
RegExp(r'apply plugin:\s*"com\.android\.library"\s*\n?'),
'',
);
content = content.replaceAll(
RegExp(r"apply plugin:\s*'kotlin-android'\s*\n?"),
'',
);
content = content.replaceAll(
RegExp(r'apply plugin:\s*"kotlin-android"\s*\n?'),
'',
);
// Insert plugins{} block at the very TOP of the file (Gradle requires it
// before any other statements including group/version assignments).
if (!content.contains('plugins {') && !content.contains('plugins{')) {
// Remove group/version from their current position (they'll move after plugins{}).
final groupVersionMatch = RegExp(
r'^group\s*=.+\nversion\s*=.+\n',
multiLine: true,
).firstMatch(content);
String groupVersionBlock = '';
if (groupVersionMatch != null) {
groupVersionBlock = groupVersionMatch.group(0)!;
content = content.replaceFirst(groupVersionBlock, '');
}
content =
'plugins {\n id "com.android.library"\n id "org.jetbrains.kotlin.android"\n}\n\n${groupVersionBlock.trim().isEmpty ? "" : "${groupVersionBlock.trim()}\n\n"}${content.trimLeft()}';
}
// Fix ndkVersion = android.ndkVersion → hardcoded version for standalone builds.
content = content.replaceAll(
'ndkVersion = android.ndkVersion',
'ndkVersion = "27.0.12077973"',
);
// Collapse sequences of 3+ blank lines to a single blank line (cosmetic cleanup).
content = content.replaceAll(RegExp(r'\n{3,}'), '\n\n');
modified = true;
}
final srcDirsLine = isKts
? r' kotlin.srcDirs += setOf("${project.projectDir}/../lib/src/generated/kotlin")'
: r' kotlin.srcDirs += "${project.projectDir}/../lib/src/generated/kotlin"';
// 1. Ensure kotlin.srcDirs for generated Kotlin bridges.
// .bridge.g.kt files live in lib/src/generated/kotlin/ — Gradle must see
// that directory as a Kotlin source root or the JNI bridge classes won't compile.
// Note: add to kotlin.srcDirs ONLY, NOT java.srcDirs — in AGP 8.x, routing
// .kt through the Java compiler path causes "Unresolved reference: XxxJniBridge".
if (!content.contains('generated/kotlin')) {
final sourceSetsMatch = RegExp(r'\bsourceSets\s*\{').firstMatch(content);
if (sourceSetsMatch != null) {
// sourceSets block exists — look for main {} inside it.
final afterSourceSets = content.substring(sourceSetsMatch.end);
final mainInBlock = RegExp(r'\bmain\s*\{').firstMatch(afterSourceSets);
if (mainInBlock != null) {
final mainAbsStart = sourceSetsMatch.end + mainInBlock.start;
// Find the { of main {} and then its matching }
final openBrace = content.indexOf(
'{',
mainAbsStart + mainInBlock.group(0)!.length - 1,
);
if (openBrace >= 0) {
final mainClose = _findBlockEnd(content, openBrace);
if (mainClose > 0) {
content = content.replaceRange(
mainClose,
mainClose,
'\n$srcDirsLine\n ',
);
modified = true;
}
}
} else {
// sourceSets exists but no main {} — add main {} before sourceSets closing brace
final sourceSetsClose = _findBlockEnd(content, sourceSetsMatch.end - 1);
if (sourceSetsClose > 0) {
content = content.replaceRange(
sourceSetsClose,
sourceSetsClose,
' main {\n$srcDirsLine\n }\n ',
);
modified = true;
}
}
} else {
// No sourceSets block — inject one inside android {}
final androidMatch = RegExp(r'\bandroid\s*\{').firstMatch(content);
if (androidMatch != null) {
content = content.replaceRange(
androidMatch.end,
androidMatch.end,
'\n sourceSets {\n main {\n$srcDirsLine\n }\n }',
);
} else {
content +=
'\nandroid {\n sourceSets {\n main {\n$srcDirsLine\n }\n }\n}\n';
}
modified = true;
}
}
// 2. Ensure kotlinOptions { jvmTarget = "17" } for correct bytecode target.
if (!content.contains('kotlinOptions')) {
final androidMatch = RegExp(r'\bandroid\s*\{').firstMatch(content);
if (androidMatch != null) {
content = content.replaceRange(
androidMatch.end,
androidMatch.end,
'\n kotlinOptions { jvmTarget = "17" }',
);
modified = true;
}
}
// 3. Ensure kotlinx-coroutines (required for generated Kotlin suspend bridge functions).
if (!content.contains('kotlinx-coroutines')) {
final depsMatch = RegExp(r'\bdependencies\s*\{').firstMatch(content);
if (depsMatch != null) {
content = content.replaceRange(
depsMatch.end,
depsMatch.end,
'\n implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"\n implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"',
);
} else {
content +=
'\ndependencies {\n implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"\n implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"\n}\n';
}
modified = true;
}
if (modified) buildGradle.writeAsStringSync(content);
}