teads_flutter 1.1.0 copy "teads_flutter: ^1.1.0" to clipboard
teads_flutter: ^1.1.0 copied to clipboard

Teads in-app ads for Flutter: Feed, Media, Banner, Interstitial and Recommendations placements over the native Teads iOS and Android SDKs.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:teads_flutter/teads_flutter.dart';

import 'screens/article_screen.dart';
import 'screens/config_screen.dart';
import 'screens/feed_screen.dart';
import 'screens/interstitial_screen.dart';
import 'screens/recommendations_screen.dart';

/// Verification/CI hook: launch straight into the article page, which hosts the
/// wrapped Media placement (`--dart-define=AUTO_OPEN_ARTICLE=true`).
const bool autoOpenArticle = bool.fromEnvironment('AUTO_OPEN_ARTICLE');

/// Same, for the Dart-implemented Feed (`--dart-define=AUTO_OPEN_FEED=true`).
///
/// Worth more on iOS than on Android: the simulator has no scriptable tap
/// injection (no `adb shell input` equivalent), so without this there is no way to
/// reach the screen from a script at all.
const bool autoOpenFeed = bool.fromEnvironment('AUTO_OPEN_FEED');

/// Same, for the wrapped Interstitial (`--dart-define=AUTO_OPEN_INTERSTITIAL=true`).
///
/// Needed to inspect the Activity/task structure the interstitial presents itself
/// into: reaching it by tap means scrolling the config screen to a button whose
/// position moves, which is not something a script should depend on.
const bool autoOpenInterstitial =
    bool.fromEnvironment('AUTO_OPEN_INTERSTITIAL');

/// Same, for Recommendations (`--dart-define=AUTO_OPEN_RECOMMENDATIONS=true`).
///
/// Recommendations has no config-screen widget-index footgun to avoid, but the
/// same "reach it without a fragile tap sequence" reasoning applies.
const bool autoOpenRecommendations =
    bool.fromEnvironment('AUTO_OPEN_RECOMMENDATIONS');

/// Frame-timing harness (`--dart-define=FRAME_STATS=true`).
///
/// Exists because the obvious tools do not work here. `Choreographer: Skipped N
/// frames` only fires past ~30 dropped frames, so it is blind to the one- and
/// two-frame hitches that read as "a bit janky", and `dumpsys gfxinfo` reports
/// "Total frames rendered: 0" for a Flutter app because Impeller does not draw
/// through Android's HWUI pipeline at all. Flutter's own frame timings are the only
/// honest source, so this reads them directly.
const bool frameStats = bool.fromEnvironment('FRAME_STATS');

/// Reports build and raster percentiles every few seconds while scrolling.
///
/// Build and raster are separated on purpose: they fail for different reasons and
/// point at different fixes. A slow BUILD is Dart-side work — layout, setState
/// churn. A slow RASTER is the compositor, which is where a platform view the size
/// of a paginating feed shows up.
void _startFrameStats() {
  final build = <int>[];
  final raster = <int>[];

  SchedulerBinding.instance.addTimingsCallback((List<FrameTiming> timings) {
    for (final timing in timings) {
      build.add(timing.buildDuration.inMicroseconds);
      raster.add(timing.rasterDuration.inMicroseconds);
    }
  });

  // The frame budget has to come from the panel, not a constant. A hardcoded
  // 16.7ms is the 60Hz budget, and on a 120Hz phone it is twice too generous:
  // 9-12ms frames pass the check while actually missing vsync, which is exactly
  // the one- and two-frame hitch that reads as "a bit janky". Read per tick
  // rather than once, because these panels switch rate adaptively and the rate
  // during THIS window is the one the samples should be judged against.
  double refreshRate() {
    final views = SchedulerBinding.instance.platformDispatcher.views;
    if (views.isEmpty) return 60;
    final rate = views.first.display.refreshRate;
    return rate > 0 ? rate : 60;
  }

  String summarise(String label, List<int> samples, double budgetUs) {
    final sorted = List<int>.of(samples)..sort();
    String at(double q) =>
        (sorted[(sorted.length * q).clamp(0, sorted.length - 1).toInt()] / 1000)
            .toStringAsFixed(1);
    final over = samples.where((us) => us > budgetUs).length;
    return '$label p50=${at(0.5)}ms p90=${at(0.9)}ms '
        'worst=${(sorted.last / 1000).toStringAsFixed(1)}ms over=$over';
  }

  Timer.periodic(const Duration(seconds: 3), (_) {
    if (build.isEmpty) return;
    final hz = refreshRate();
    final budgetUs = 1000000 / hz;
    // The one print deliberately kept. This harness exists solely to emit it, and
    // it only runs when FRAME_STATS is passed at build time — so it is opt-in
    // diagnostics rather than the trace logging that was removed everywhere else.
    debugPrint('FRAME STATS n=${build.length} '
        'hz=${hz.toStringAsFixed(1)} '
        'budget=${(budgetUs / 1000).toStringAsFixed(1)}ms | '
        '${summarise("build", build, budgetUs)} | '
        '${summarise("raster", raster, budgetUs)}');
    build.clear();
    raster.clear();
  });
}

/// Test IDs used by the launch hooks above and as the config screen's defaults.
const _feedConfig = FeedPlacementConfig(
  widgetId: 'MB_1',
  installationKey: 'NANOWDGT01',
  widgetIndex: 0,
  articleUrl: 'https://mobile-demo.outbrain.com/',
);

const _mediaConfig = MediaPlacementConfig(
  pid: 84242,
  articleUrl: 'https://example.com/article',
);

// INT_MW_1 rather than the Feed's MB_1: the widget id is what selects the format,
// so reusing the Feed id here would serve a feed into a fullscreen slot.
const _interstitialConfig = InterstitialPlacementConfig(
  articleUrl: 'https://mobile-demo.outbrain.com/',
  widgetId: 'INT_MW_1',
  installationKey: 'NANOWDGT01',
);

// SDK_1: matches the native iOS demo's default Recommendations widget id
// (AdPlacementViewModel.recommendationsConfig).
const _recommendationsConfig = RecommendationsPlacementConfig.url(
  widgetId: 'SDK_1',
  articleUrl: 'https://mobile-demo.outbrain.com/',
);

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  try {
    await TeadsSdk.configure('iOSSampleApp2014');
  } catch (_) {
    // Swallowed so the demo still opens; the placements report their own failures.
  }

  try {
    // Only the admob_*_screen.dart demos need this; the config screen itself
    // requests no ad, so a slow or failed init here cannot block the app opening.
    await MobileAds.instance.initialize();
  } catch (_) {
    // Best-effort, same reasoning as TeadsSdk.configure above.
  }

  if (frameStats) _startFrameStats();

  runApp(const InAppSdkFlutterDemoApp());

  // ATT is requested AFTER the first frame, and awaiting it before runApp is a
  // trap worth spelling out: iOS can only present the prompt once the app is
  // active, so asking while there is still no UI can leave the completion
  // handler unfired. `main()` then never reaches runApp and the app hangs on a
  // white screen - which is exactly what happened on a physical iPhone, while
  // the simulator looked fine because its ATT status was already determined and
  // the call returned straight away.
  //
  // Still resolved before any ad request, which is the ordering that matters:
  // the advertising id is read when a placement assembles its request, and the
  // config screen is what comes up first - it builds no placement. Null means
  // Android, which has no ATT prompt.
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    try {
      await TeadsPrivacy.requestTrackingAuthorization();
    } catch (_) {
      // Best-effort: a refusal or a failure here must not hold up the app.
    }
  });
}

class InAppSdkFlutterDemoApp extends StatelessWidget {
  const InAppSdkFlutterDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'InApp SDK Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: _home(),
    );
  }

  Widget _home() {
    if (autoOpenFeed) {
      return const FeedScreen(config: _feedConfig);
    }
    if (autoOpenArticle) {
      return const ArticleScreen(mediaConfig: _mediaConfig);
    }
    if (autoOpenInterstitial) {
      return const InterstitialScreen(config: _interstitialConfig);
    }
    if (autoOpenRecommendations) {
      return const RecommendationsScreen(config: _recommendationsConfig);
    }
    return const ConfigScreen();
  }
}
0
likes
160
points
404
downloads

Documentation

API reference

Publisher

verified publisherteads.com

Weekly Downloads

Teads in-app ads for Flutter: Feed, Media, Banner, Interstitial and Recommendations placements over the native Teads iOS and Android SDKs.

Homepage

Topics

#ads #advertising #monetization #video

License

Apache-2.0 (license)

Dependencies

battery_plus, connectivity_plus, device_info_plus, flutter, package_info_plus, url_launcher, webview_flutter, webview_flutter_android, webview_flutter_platform_interface, webview_flutter_wkwebview

More

Packages that depend on teads_flutter

Packages that implement teads_flutter