linkKotlinPlugin function

void linkKotlinPlugin(
  1. String pluginName,
  2. List<Map<String, String>> modules, {
  3. String baseDir = '.',
})

Implementation

void linkKotlinPlugin(
  String pluginName,
  List<Map<String, String>> modules, {
  String baseDir = '.',
}) {
  final kotlinDir = Directory(
    p.join(baseDir, 'android', 'src', 'main', 'kotlin'),
  );
  if (!kotlinDir.existsSync()) return;
  final pluginFiles = kotlinDir
      .listSync(recursive: true, followLinks: false)
      .whereType<File>()
      .where((f) => !f.path.contains('.symlinks'))
      .where((f) => f.path.endsWith('Plugin.kt'))
      .toList();
  if (pluginFiles.isEmpty) return;
  final pluginFile = pluginFiles.first;
  var content = pluginFile.readAsStringSync();
  bool modified = false;
  for (final m in modules) {
    final name = m['module']!;
    final lib = (m['lib'] ?? name.toLowerCase()).replaceAll('-', '_');
    final reg = '${name}JniBridge';
    final impl = '${name}Impl';
    // The Kotlin generator emits: package nitro.${lib}_module
    // so the fully-qualified import is: import nitro.${lib}_module.${Module}JniBridge
    final importLine = 'import nitro.${lib}_module.$reg';

    // ── 1. Ensure import is present ─────────────────────────────────────────
    if (!content.contains(importLine)) {
      // Insert after the last 'import …' line in the file for clean ordering.
      final importMatches = RegExp(
        r'^import .+$',
        multiLine: true,
      ).allMatches(content);
      if (importMatches.isNotEmpty) {
        final lastImport = importMatches.last;
        content = content.replaceRange(
          lastImport.end,
          lastImport.end,
          '\n$importLine',
        );
      } else {
        // No imports yet — add one blank line after the package declaration.
        content = content.replaceFirstMapped(
          RegExp(r'^package .+$', multiLine: true),
          (m) => '${m.group(0)!}\n\n$importLine',
        );
      }
      modified = true;
    }

    // ── 2. Ensure register() call is present ────────────────────────────────
    // Detect whether XxxImpl needs a Context argument by scanning the impl file.
    // Nitro Kotlin impls commonly take Context in their primary constructor.
    // If we inject XxxImpl() when XxxImpl(context: Context) is required, the
    // call compiles but crashes at runtime — pass binding.applicationContext.
    final implArg = _detectKotlinImplArg(impl, baseDir: baseDir);
    final registerCall = '$reg.register($impl($implArg))';
    if (!content.contains('$reg.register')) {
      final match = RegExp(
        r'\w+JniBridge\.register\(.*?\)\)',
      ).allMatches(content);
      if (match.isNotEmpty) {
        // Append after the last existing JniBridge.register() call.
        content = content.replaceFirst(
          match.last.group(0)!,
          '${match.last.group(0)!}\n        $registerCall',
        );
      } else {
        content = content.replaceFirst(
          'override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {',
          'override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        $registerCall',
        );
      }
      modified = true;
    }
  }
  if (modified) pluginFile.writeAsStringSync(content);
}