Skip to main content

Start.io Flutter Integration: Solving 'Black Screen' & Native View Issues

 Few things destroy a developer’s momentum like a successful compile followed by a broken UI. You’ve integrated the Start.io (formerly StartApp) SDK, the logs confirm "Ad Loaded," yet your device displays a void: a black or transparent container where a banner should be.

This is a specific failure mode common in cross-platform development. It usually stems from a disconnect between Flutter’s rendering engine (Skia/Impeller) and the Native Platform Views required to display ad networks.

This guide provides a rigorous technical breakdown of why this happens and creates a production-ready implementation to fix missing metadata, incorrect platform view rendering, and lifecycle management.

The Root Cause: Hybrid Composition & Context

To understand the fix, you must understand the architecture. Flutter renders its UI to a canvas. However, Start.io (and AdMob/Unity Ads) serves ads using Native Views (android.view.View on Android, UIView on iOS).

When you place a banner in Flutter, you are asking the engine to punch a hole in the Flutter canvas and embed a native component. This utilizes Platform Views.

The "Black Screen" or "Empty Container" issue typically arises from three specific failures:

  1. Ambiguous Constraints: The native view has loaded, but Flutter has calculated a height of 0.0 or null for the container.
  2. Missing Manifest Metadata: The SDK initializes but lacks the Android Activity context or Application ID required to render the visual assets.
  3. Hybrid Composition Failures: The texture layer used to composite the native view fails to synchronize with the Flutter frame, often due to hardware acceleration conflicts on specific Android versions.

Step 1: Android Manifest Configuration (The Primary Culprit)

The most common reason for a black ad unit on Android is a missing App ID reference in the AndroidManifest.xml. While the SDK allows programmatic initialization in Dart, the native Android rendering thread looks for metadata tags before the Flutter engine fully spins up.

Open android/app/src/main/AndroidManifest.xml and ensure the following configuration exists inside the <application> tag.

The Fix:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yourcompany.yourapp">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <!-- Required for better ad targeting and fill rates -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <application
        android:label="your_app_label"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

        <!-- 1. STRICTLY REQUIRED: Your Start.io App ID -->
        <meta-data
            android:name="com.startapp.sdk.APPLICATION_ID"
            android:value="YOUR_ANDROID_APP_ID_HERE" />

        <!-- 2. Disable Hardware Acceleration for the Ad Activity if experiencing black flickering -->
        <activity android:name="com.startapp.sdk.adsbase.activities.OverlayActivity"
                  android:theme="@android:style/Theme.Translucent"
                  android:configChanges="orientation|keyboardHidden|screenSize" />

        <activity android:name="com.startapp.sdk.adsbase.activities.FullScreenActivity"
                  android:theme="@android:style/Theme.Light"
                  android:configChanges="orientation|keyboardHidden|screenSize" />

        <!-- Standard Flutter Activity -->
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Intent filters -->
        </activity>
    </application>
</manifest>

Why this works: Explicitly declaring OverlayActivity ensures the SDK has a registered context to draw over the Flutter application. Without this, the SDK attempts to attach a view to a non-existent window, resulting in a black void.

Step 2: iOS Info.plist & SKAdNetwork

On iOS, "black screens" often equate to App Transport Security (ATS) blocking the asset download, or missing privacy attributions.

Open ios/Runner/Info.plist.

The Fix:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- Basic Start.io Configuration -->
    <key>com.startapp.sdk.APPLICATION_ID</key>
    <string>YOUR_IOS_APP_ID_HERE</string>

    <!-- App Transport Security Settings -->
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>

    <!-- SKAdNetwork is CRITICAL for iOS 14+ monetization and rendering -->
    <key>SKAdNetworkItems</key>
    <array>
        <dict>
            <key>SKAdNetworkIdentifier</key>
            <string>5l3tpt7t6e.skadnetwork</string>
        </dict>
        <dict>
            <key>SKAdNetworkIdentifier</key>
            <string>mlmmfzh3r3.skadnetwork</string>
        </dict>
        <!-- Add additional network IDs as recommended by Start.io docs -->
    </array>
    
    <!-- Privacy Descriptions -->
    <key>NSUserTrackingUsageDescription</key>
    <string>We use this identifier to deliver personalized ads relevant to you.</string>
</dict>
</plist>

Step 3: The Robust Flutter Implementation

Now that the native layer is prepped, we must handle the Flutter implementation. A common mistake is placing the StartAppBanner inside a flexible widget (like Column or ListView) without explicit sizing.

Native Platform Views cannot inherently "wrap content" because Flutter calculates layout before the native view renders. You must wrap the banner in a specific constraint.

Dependency

Ensure you are using the latest version in pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  startapp_sdk: ^2.5.0 # Verify latest version on pub.dev

The Wrapper Component

Create a reusable file lib/widgets/ad_banner.dart. This component handles initialization checks and layout constraints to prevent zero-height rendering.

import 'package:flutter/material.dart';
import 'package:startapp_sdk/startapp_sdk.dart';

class RobustStartIoBanner extends StatefulWidget {
  final VoidCallback? onAdLoaded;
  final VoidCallback? onAdFailedToLoad;

  const RobustStartIoBanner({
    super.key,
    this.onAdLoaded,
    this.onAdFailedToLoad,
  });

  @override
  State<RobustStartIoBanner> createState() => _RobustStartIoBannerState();
}

class _RobustStartIoBannerState extends State<RobustStartIoBanner> {
  final StartAppSdk _startAppSdk = StartAppSdk();
  StartAppBannerAd? _bannerAd;
  bool _isAdLoaded = false;
  bool _hasError = false;

  @override
  void initState() {
    super.initState();
    _loadBanner();
  }

  Future<void> _loadBanner() async {
    try {
      // 1. Load the banner explicitly
      // Note: StartAppSdk automatically handles singleton initialization 
      // based on Manifest/Plist data, but you can pass appId here if needed.
      _bannerAd = await _startAppSdk.loadBannerAd(
        StartAppBannerType.BANNER,
        prefs: const StartAppAdPreferences(
          adTag: 'home_screen_bottom', // Good for analytics
        ),
        onAdDisplayed: () {
          debugPrint('Start.io: Banner Displayed');
          if (mounted) {
            setState(() => _isAdLoaded = true);
            widget.onAdLoaded?.call();
          }
        },
        onAdNotDisplayed: () {
          debugPrint('Start.io: Banner failed to render');
          if (mounted) {
            setState(() => _hasError = true);
            widget.onAdFailedToLoad?.call();
          }
        },
      );
    } catch (e) {
      debugPrint('Start.io Error: $e');
      if (mounted) {
        setState(() => _hasError = true);
      }
    }
  }

  @override
  void dispose() {
    // 2. Critical: Dispose native resources to prevent memory leaks
    _bannerAd?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_hasError) {
      // Fail gracefully - return empty box to collapse layout
      return const SizedBox.shrink();
    }

    if (!_isAdLoaded || _bannerAd == null) {
      // Placeholder while loading
      return const SizedBox(
        height: 50,
        width: double.infinity,
        child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
      );
    }

    // 3. The Container Fix
    // Wrapping in a SizedBox with explicit height prevents "Zero Height" logic
    // from the Flutter layout engine.
    return SizedBox(
      height: 50, // Standard Banner Height
      width: double.infinity,
      child: StartAppBanner(_bannerAd!),
    );
  }
}

Step 4: ProGuard Rules (The Release Build Killer)

A scenario often overlooked: the ads work perfectly in debug mode, but turn black or crash the app in release mode.

This happens because R8 (Android's code shrinker) obfuscates the Start.io Java/Kotlin classes. If the SDK cannot find its classes via reflection, the view fails to inflate.

Open android/app/proguard-rules.pro (create it if it doesn't exist) and add:

# Start.io / StartApp SDK Rules
-keep class com.startapp.** { *; }
-keep interface com.startapp.** { *; }
-keepattributes Signature,InnerClasses,EnclosingMethod

# If using specific adapters
-dontwarn com.startapp.**

Ensure your android/app/build.gradle includes this rule file:

buildTypes {
    release {
        // ... signing config
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

Summary of Fixes

If you are seeing a black screen, verify this checklist in order:

  1. Metadata: Is com.startapp.sdk.APPLICATION_ID present in AndroidManifest.xml?
  2. Constraints: Is your Flutter widget wrapped in a SizedBox with a defined height (e.g., 50 or 90)?
  3. Obfuscation: Did you add the -keep rules to ProGuard for your release build?
  4. Hardware Acceleration: If the view flickers black, try disabling hardware acceleration specifically for the Start.io activities in the manifest.

By treating the ad unit as a hosted Native View rather than a standard Flutter widget, you ensure the rendering pipeline has the context and constraints necessary to display the asset, securing your ad revenue and user experience.