generateAndroidManifestFromSpec function

String generateAndroidManifestFromSpec({
  1. required String packageName,
  2. required String label,
  3. required String minSdk,
  4. required String targetSdk,
  5. String activityName = '.MainActivity',
  6. ManifestSpec spec = const ManifestSpec(),
  7. String extraIntentFilters = '',
  8. String iconRef = '',
})

Renders the host AndroidManifest from a typed ManifestSpec (ADR-0006).

Implementation

String generateAndroidManifestFromSpec({
  required String packageName,
  required String label,
  required String minSdk,
  required String targetSdk,
  String activityName = '.MainActivity',
  ManifestSpec spec = const ManifestSpec(),
  String extraIntentFilters = '',
  String iconRef = '',
}) {
  final permLines = spec.permissions
      .map((p) => '    <uses-permission android:name="$p" />')
      .join('\n');

  // Application attributes: typed spec fields first, then passthrough map.
  final appAttrs = <String, String>{
    'android:label': label,
    if (iconRef.isNotEmpty) 'android:icon': iconRef,
    'android:debuggable': '${spec.debuggable}',
    'android:extractNativeLibs': '${spec.extractNativeLibs}',
    if (spec.cleartextTraffic != null)
      'android:usesCleartextTraffic': '${spec.cleartextTraffic}',
    ...spec.applicationAttributes,
  };
  final appAttrLines = appAttrs.entries
      .map((e) => '        ${e.key}="${e.value}"')
      .join('\n');

  final metaData = <MetaDataSpec>[
    ...spec.applicationMetaData,
    if (spec.flutterDeeplinking)
      const MetaDataSpec(name: 'flutter_deeplinking_enabled', value: 'true'),
  ];
  final metaDataLines = metaData
      .map(
        (m) => m.resource != null
            ? '        <meta-data android:name="${m.name}" '
                  'android:resource="${m.resource}" />'
            : '        <meta-data android:name="${m.name}" '
                  'android:value="${m.value}" />',
      )
      .join('\n');

  // Raw application-level elements (services, receivers, providers, ...).
  final appElements = spec.applicationElements
      .map((e) => '        $e')
      .join('\n');

  final actAttrLines = spec.activityAttributes.entries
      .map((e) => '            ${e.key}="${e.value}"')
      .join('\n');

  // Activity-level meta-data; a spec-provided NormalTheme replaces the
  // hardcoded `@android:style` fallback (project launch themes).
  final hasNormalTheme = spec.activityMetaData.any(
    (m) => m.name == 'io.flutter.embedding.android.NormalTheme',
  );
  final actMetaDataLines = spec.activityMetaData
      .map(
        (m) => m.resource != null
            ? '            <meta-data android:name="${m.name}" '
                  'android:resource="${m.resource}" />'
            : '            <meta-data android:name="${m.name}" '
                  'android:value="${m.value}" />',
      )
      .join('\n');

  // Hardcoded embedding fallback only when the spec does not define its own
  // NormalTheme (project launch themes, e.g. @style/NormalTheme).
  final normalThemeMetaData = hasNormalTheme
      ? ''
      : '''
            <meta-data
                android:name="io.flutter.embedding.android.NormalTheme"
                android:resource="@android:style/Theme.Light.NoTitleBar" />''';

  // Raw manifest-level elements (queries, uses-feature, ...).
  final rawElements = spec.manifestElements.join('\n');

  final filters = <String>[
    if (spec.deeplinks.isNotEmpty) renderDeeplinkIntentFilters(spec.deeplinks),
    if (extraIntentFilters.isNotEmpty) extraIntentFilters,
  ].join('\n');

  return '''
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="$packageName">

    <uses-sdk
        android:minSdkVersion="$minSdk"
        android:targetSdkVersion="$targetSdk" />

$permLines
$rawElements

    <application
$appAttrLines>
        <activity
            android:name="$activityName"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@android:style/Theme.Light.NoTitleBar"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
$actAttrLines
$actMetaDataLines$normalThemeMetaData
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
$filters
        </activity>
$metaDataLines
$appElements
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>
''';
}