displayio_sdk 0.1.0
displayio_sdk: ^0.1.0 copied to clipboard
Flutter plugin for the DIO (Display.io) ad SDKs — banner, infeed, interstitial, interscroller, audio and native ads on Android and iOS through one Dart API.
displayio_sdk #
The DIO (Display.io) Flutter plugin. It wraps the native DIO ad SDKs for Android and iOS, so a Flutter app loads and shows DIO ads through a single Dart API — every DIO ad format, one call site.
Status: all formats are implemented and device-verified on both platforms. The package is pre-1.0 — the API may still change.
Requirements #
| Flutter | >= 3.44.1 (Dart SDK ^3.12.1) |
| Android | minSdk 24; pulls com.brandio.ads:sdk from https://maven.display.io/ |
| iOS | minimum deployment target 15.0; DIOSDK via Swift Package Manager |
The Android artifact lives in DIO's own Maven repo, so add it to your app's
settings.gradle.kts (dependencyResolutionManagement) or build.gradle.kts:
repositories {
maven { url = uri("https://maven.display.io/") }
}
You need a DIO app id and placement ids from the DIO dashboard.
Install #
dependencies:
displayio_sdk: ^0.1.0
import 'package:displayio_sdk/displayio_sdk.dart';
Quick start #
Initialize once at startup, then load a placement. The returned DioAd is a
sealed type — switch on it to get the right presentation:
await DioSdk.instance.initialize(appId: 'APP_ID');
final ad = await DioSdk.instance.loadAd('PLACEMENT_ID');
switch (ad) {
case DioInlineAd(): // banner / infeed / inline / interscroller
DioAdView(ad: ad, reveal: true);
case DioInterstitialAd():
await ad.show();
case DioAudioAd(): // InFlowAudio / InRing
ad.play();
ad.pause();
ad.companionView; // DioCompanionView? — null when absent
case DioInGameAudioAd(): // the SDK's own square card, you position it
Positioned(top: 16, right: 16, child: ad.cardView);
case DioNativeAd():
DioNativeAdView(ad: ad, child: /* your layout + slot widgets */);
}
The concrete ad type comes from ad.type, not from the placement type — an
inline placement resolves at load time to a banner, infeed, or interscroller.
On iOS the first
initializecall blocks the main thread briefly (the native SDK creates aWKWebViewsynchronously to read the user agent), which stalls Flutter rendering. Call it at startup / on a splash screen, and judge UI smoothness in profile or release builds, not debug.
Formats #
| Format | Presentation |
|---|---|
| Banner (HTML / video / audio) | DioAdView |
| Infeed (video / display / audio) | DioAdView |
| Interscroller (display / video / audio) | DioAdView(reveal: true) — Dart-driven reveal parallax |
| Inline | DioAdView (resolves to one of the above) |
| Interstitial (display / video / audio) | ad.show() |
| InFlowAudio / InRing | ad.play() / ad.pause() + optional companion view |
| In-Game Audio | ad.cardView — the SDK's own card, positioned by you |
| Native | DioNativeAdView + slot widgets |
Native ads #
Native ads are publisher-rendered: the SDK gives you the text assets and fills its own media / icon / CTA slots; you build the layout with your own Flutter widgets.
DioNativeAdView(
ad: ad,
child: Column(
children: [
Text(ad.headline ?? ''),
DioNativeIconView(ad: ad, size: 44), // optional
DioNativeMediaView(ad: ad, aspectRatio: 16 / 9), // required — click + viewability root
Text(ad.body ?? ''),
DioNativeCtaView(ad: ad, text: ad.callToAction), // optional, separately tracked CTA
],
),
);
DioNativeAdViewregisters and unregisters the slots for click and impression tracking; wrapping the layout in it is required.- Text (
headline,body,callToAction,advertiser,price,privacy) is plain Flutter. There are no image URLs — icon and main image are rendered by the SDK into native slots, and click/viewability need real native views. - A Flutter-drawn CTA cannot be click-tracked; use
DioNativeCtaViewor rely on the media tap.
In-Game Audio #
An audio ad that the SDK renders as its own square card — gradient artwork,
an animated equalizer, an AD badge, a progress ring and a native mute control.
It is neither an inline ad (it takes no slot in the content flow) nor a headless
audio ad (there is no play / pause — playback follows attachment and
on-screen visibility, and the user mutes from the card).
You position the card yourself, which is the whole point of the format:
Stack(children: [
gameSurface,
Positioned(top: 16, right: 16, child: ad.cardView),
]);
ad.preferredSize; // the card's natural (square) size — cardView uses it
await ad.dispose(); // or just remove the widget: detaching ends the ad
Resize it through the placement, not the widget:
await DioSdk.instance.loadAd('PID', options: const DioAdOptions(
inGameAudio: DioInGameAudioConfig(customWidth: 160), // the square's side
audio: DioAudioControls(showSoundControl: true), // iOS mute control
));
DioInGameAudioView(width:, height:) sizes the slot, not the creative —
anything below preferredSize crops the progress ring and the mute control,
which sit at the card's edges.
The mute control is asymmetric because the native SDKs are: on Android it is a
view flag (DioInGameAudioView(showSoundControl: true), the default), on iOS a
placement flag set before the request (DioAudioControls.showSoundControl).
Showing it matters — with the control hidden, some creatives' decorative audio
bars act as the ad's click target, so a tap meant to mute counts as a click.
If the creative has an end-card, the SDK swaps it into the same card; there is no separate companion widget.
Customization — DioAdOptions #
Per-load options span two axes: placement styling and the ORTB ad
request. Every field is nullable and null means leave the native SDK
default untouched.
final ad = await DioSdk.instance.loadAd('PID', options: DioAdOptions(
// placement styling — only the group matching the resolved format applies
audio: DioAudioControls(showSoundControl: false, accentColor: Color(0xFF00A2FF)),
interscroller: DioInterscrollerConfig(headerText: 'Ad', showTapHint: false),
infeed: DioInfeedConfig(fullWidth: true, ctaButtonInfeedColor: Colors.black),
pureAudioAd: DioPureAudioAdConfig(autoRequestEnabled: false),
inGameAudio: DioInGameAudioConfig(customWidth: 160),
native: DioNativeRequestConfig(video: DioAssetParams(required: true)),
// ORTB bid request / targeting — always applied
request: DioAdRequestConfig(
user: DioRequestUser(yob: 1990, gender: DioGender.female),
bcat: ['IAB25'],
tmax: 1000,
),
));
Ad metadata #
Each loaded ad carries a read-only snapshot of the bid response:
ad.metadata?.ecpm;
ad.metadata?.advertiserName;
ad.metadata?.creativeId;
ad.description; // "AdUnit: <type>, Placement id: <id>, Request id: <id>"
Placement listing (diagnostics) #
final placements = await DioSdk.instance.placements();
// [16209 (inGameAudio), 5133 (interstitial), 6363 (mediumrectangle), ...]
What the dashboard returned for the initialized app id — useful when a typo in
the app id would otherwise only surface as a no-fill. It returns an empty list
before initialize and on any native error, and never throws, so a diagnostic
screen calling it from initState cannot break. DioPlacement.type is null for
a unit this plugin does not model (the deprecated ones); rawType keeps the
native string either way. It carries no ad state by design — the native SDK
stays the single source of truth for that.
Server-to-server (ORTB) #
For a mediation / S2S flow, let the SDK build the bid request, POST it to your own exchange, and render the response you get back:
final requestJson = await DioSdk.instance.buildOrtbRequest('PID');
final token = await DioSdk.instance.token(); // DIO user token (user.buyeruid)
// ... POST requestJson to your exchange, receive ortbJson ...
final ad = await DioSdk.instance.loadAdFromOrtb('PID', ortbJson);
The result is an ordinary DioAd — the same widgets, show, events, metadata,
and dispose apply. loadAdFromOrtb takes the raw ORTB JSON string; only
the placement part of options applies (including native asset params, which
the parser needs so asset ids match).
Known limitations #
- iOS interscroller does not auto-pause/resume media on scroll visibility. That logic is scroll-driven inside the native SDK, and the plugin hosts the interscroller in a scroll view whose offset is pinned (the parallax is Dart-driven), so the SDK never sees a visibility change. Android is unaffected.
*TextSizeoptions are Android-only — iOS folds text size into a font the SDK does not expose.- Custom fonts are not exposed. A Flutter-bundled font is invisible to
native
Typeface/UIFontwithout registering it in the native project, which would break the single-Dart-API promise. interscroller/infeedstyling applies only when the placement resolves directly to that format — reached through aninlineplacement, the concrete sub-placement is unknown at config time and the styling is skipped.- Audio output routing is the publisher's job on iOS (a global
AVAudioSession);setAudioOutputis a no-op there. Seeexample/for a demo using theaudio_sessionpackage. - Native ads use up to three platform views (media / icon / CTA) — mind the count in a scrolling feed, and note that viewability is measured on the media slot rather than the whole card.
Example #
example/ is a full test app covering every format, the customization and ORTB
flows, and metadata:
cd example && flutter run
Placement inventory lives in example/lib/test_placements.dart and ad
customization in example/lib/test_ad_config.dart.
License #
MIT — see LICENSE.