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.2.1
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; or ad.play() / ad.pause() with the card off |
| 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 takes no slot in the content flow: playback follows attachment and on-screen
visibility, and the user mutes from the card. The card's look is yours to change,
and it can be dropped entirely — see below.
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. To keep the card on its own artwork, turn the companion off and the SDK does not even load it:
inGameAudio: DioInGameAudioConfig(companionEnabled: false),
Styling the card
Every colour is optional — leave one out and the SDK keeps its own default.
await DioSdk.instance.loadAd('PID', options: const DioAdOptions(
inGameAudio: DioInGameAudioConfig(
backgroundTopLeft: Color(0xFF7B2FF7),
backgroundBottomRight: Color(0xFFF107A3),
accentColor: Color(0xFFFFE600), // bars, now-playing glyph and progress ring
badgeBackgroundColor: Color(0xFFFFE600),
badgeTextColor: Color(0xFF000000),
cornerRadius: 20,
ringWidth: 4,
),
));
Individual elements can be switched off with showAdBadge,
showNowPlayingGlyph and showProgressRing.
Your own image in the card
Pass encoded PNG or JPEG bytes. The image fills the card's content inset by
iconPadding, with the background showing through the gap, and replaces the
default audio bars. It is decorative — it takes no touches, so the whole card
stays tappable — and the campaign's companion still wins over it when there is
one.
final bytes = (await rootBundle.load('assets/brand_mark.png'))
.buffer.asUint8List();
await DioSdk.instance.loadAd('PID', options: DioAdOptions(
inGameAudio: DioInGameAudioConfig(icon: bytes, iconPadding: 12),
));
Playing without a card
Some games have nowhere to put a card but can still carry a sponsored audio track. Turning the card off keeps the audio and drops the UI entirely:
await DioSdk.instance.loadAd('PID', options: const DioAdOptions(
inGameAudio: DioInGameAudioConfig(showCard: false),
));
await ad.play(); // nothing is heard until this
await ad.pause();
In this mode ad.hasCard is false and ad.cardView has nothing to render, so
do not put it in the tree. The impression is counted when playback actually
starts rather than on viewability, and the SDK ends the ad itself when the track
finishes. play / pause also work with a card on screen, where the SDK
otherwise drives playback from visibility.
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.
Libraries
- displayio_sdk
- The DIO (Display.io / Brandio) Flutter SDK.