magic_ad_sdk 0.0.1
magic_ad_sdk: ^0.0.1 copied to clipboard
Magic Ads SDK Flutter plugin for Android and iOS.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:magic_ad_sdk/magic_index.dart';
void main() {
runApp(const MagicSdkDemoApp());
}
class MagicSdkDemoApp extends StatelessWidget {
const MagicSdkDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1565C0)),
useMaterial3: true,
),
home: const MagicSdkDemoPage(),
);
}
}
class DemoIds {
static const String _androidAppId = '133';
static const String _iosAppId = '134';
static String get defaultAppId => _isIos ? _iosAppId : _androidAppId;
static String get splash => _isIos ? '863' : '862';
static String get interstitial => _isIos ? '865' : '864';
static String get banner => _isIos ? '869' : '868';
static String get rewarded => _isIos ? '871' : '870';
static String get landscapeRewarded => _isIos ? '873' : '872';
static String get feed => _isIos ? '867' : '866';
static bool get _isIos => defaultTargetPlatform == TargetPlatform.iOS;
}
class DemoAppBridge {
static const MethodChannel _channel = MethodChannel('magic_sdk_demo');
static Future<String?> getIDFA() {
if (defaultTargetPlatform != TargetPlatform.iOS) {
return Future<String?>.value('IDFA only available on iOS');
}
return _channel.invokeMethod<String>('getIDFA');
}
static Future<bool?> requestLocationPermission() {
return _channel.invokeMethod<bool>('requestLocationPermission');
}
}
class MagicSdkDemoPage extends StatefulWidget {
const MagicSdkDemoPage({super.key});
@override
State<MagicSdkDemoPage> createState() => _MagicSdkDemoPageState();
}
class _MagicSdkDemoPageState extends State<MagicSdkDemoPage> {
final List<StreamSubscription<MagicBaseResponse>> _homeAdSubscriptions =
<StreamSubscription<MagicBaseResponse>>[];
final Map<String, MagicAdObject> _homeAdObjects = <String, MagicAdObject>{};
MagicBannerAdObject? _homeBannerAd;
double _homeBannerHeight = 50;
String _status = '正在初始化 MagicAdSDK...';
@override
void initState() {
super.initState();
_bindHomeAdEventHandlers();
WidgetsBinding.instance.addPostFrameCallback((_) => _initializeSdk());
}
@override
void dispose() {
for (final StreamSubscription<MagicBaseResponse> subscription
in _homeAdSubscriptions) {
subscription.cancel();
}
for (final MagicAdObject adObject in _homeAdObjects.values) {
adObject.destroy();
}
_homeBannerAd?.destroy();
_homeAdObjects.clear();
super.dispose();
}
void _bindHomeAdEventHandlers() {
_homeAdSubscriptions
..add(MagicListenerManager.bannerEventHandler.listen(_handleHomeAdEvent))
..add(
MagicListenerManager.interstitialEventHandler.listen(
_handleHomeAdEvent,
),
);
}
Future<void> _handleHomeAdEvent(MagicBaseResponse event) async {
final MagicAdObject? adObject = event.adObject;
if (adObject == null) return;
final MagicBannerAdObject? bannerAd = _homeBannerAd;
if (bannerAd != null && adObject.objectID == bannerAd.objectID) {
if (event is MagicBannerResponse &&
event.bannerStatus == MagicBannerStatus.loaded) {
setState(() => _homeBannerHeight = event.realHeight ?? 50);
}
if (event.isClosed) {
setState(() => _homeBannerAd = null);
}
return;
}
if (!_homeAdObjects.containsKey(adObject.objectID)) return;
if (!event.isClosed) return;
_homeAdObjects.remove(adObject.objectID);
}
Future<void> _run(String label, Future<Object?> Function() action) async {
setState(() => _status = '$label...');
try {
final Object? value = await action();
setState(() => _status = '$label: ${value ?? 'done'}');
} on PlatformException catch (error) {
setState(() => _status = '$label failed: ${error.code} ${error.message}');
} catch (error) {
setState(() => _status = '$label failed: $error');
}
}
Future<void> _initializeSdk() async {
await _run(
'初始化',
() => MagicInitManger.initMagicSDK(appId: DemoIds.defaultAppId),
);
}
Future<String?> _showHomeAd(Future<MagicAdObject?> Function() loadAd) async {
await MagicInitManger.initMagicSDK(appId: DemoIds.defaultAppId);
final MagicAdObject? adObject = await loadAd();
if (adObject == null) return null;
if (!mounted) {
await adObject.destroy();
return null;
}
_homeAdObjects[adObject.objectID] = adObject;
try {
if (adObject is MagicInterstitialAdObject) {
await adObject.show();
} else {
throw StateError('Unsupported home ad object: ${adObject.runtimeType}');
}
return adObject.objectID;
} catch (_) {
_homeAdObjects.remove(adObject.objectID);
rethrow;
}
}
Future<String?> _loadHomeBannerOverlay() async {
final width = MediaQuery.sizeOf(context).width;
await MagicInitManger.initMagicSDK(appId: DemoIds.defaultAppId);
await _homeBannerAd?.destroy();
final bannerAd = await MagicBannerManager.loadBannerAd(
placementID: DemoIds.banner,
width: width,
);
if (bannerAd == null) return null;
if (!mounted) {
await bannerAd.destroy();
return null;
}
final height = await bannerAd.realHeight() ?? 50;
if (mounted) {
setState(() {
_homeBannerAd = bannerAd;
_homeBannerHeight = height;
});
}
return bannerAd.objectID;
}
Future<void> _openAdPage(MagicAdDemoKind kind, String tagId) async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => MagicAdShowPage(kind: kind, tagId: tagId),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MagicAdSDK Demo')),
body: Stack(
children: <Widget>[
ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_InfoCard(
title: 'Magic App ID',
value: DemoIds.defaultAppId,
helperText: 'Android 使用 133,iOS 使用 134;广告位 ID 按平台分别配置',
),
const SizedBox(height: 16),
_DemoButton(
label: '初始化 MagicAdSDK',
onPressed: () => _run(
'初始化',
() =>
MagicInitManger.initMagicSDK(appId: DemoIds.defaultAppId),
),
),
if (defaultTargetPlatform == TargetPlatform.iOS)
_DemoButton(
label: '获取 IDFA',
onPressed: () => _run('获取 IDFA', DemoAppBridge.getIDFA),
),
_DemoButton(
label: '请求地理位置',
onPressed: () =>
_run('请求地理位置', DemoAppBridge.requestLocationPermission),
),
_DemoButton(
label: '瀑布流广告',
onPressed: () =>
_openAdPage(MagicAdDemoKind.feed, DemoIds.feed),
),
_DemoButton(
label: '瀑布流自渲染广告',
onPressed: () =>
_openAdPage(MagicAdDemoKind.selfRenderFeed, DemoIds.feed),
),
_DemoButton(
label: '开屏-大-图片',
onPressed: () =>
_openAdPage(MagicAdDemoKind.splash, DemoIds.splash),
),
_DemoButton(
label: '激励视频',
onPressed: () =>
_openAdPage(MagicAdDemoKind.rewarded, DemoIds.rewarded),
),
_DemoButton(
label: '激励横视频',
onPressed: () => _openAdPage(
MagicAdDemoKind.landscapeRewarded,
DemoIds.landscapeRewarded,
),
),
_DemoButton(
label: 'Banner图片广告',
onPressed: () => _run('Banner', _loadHomeBannerOverlay),
),
_DemoButton(
label: '插屏图片广告',
onPressed: () => _run(
'插屏',
() => _showHomeAd(
() => MagicInterstitialManager.loadInterstitialAd(
placementID: DemoIds.interstitial,
),
),
),
),
_DemoButton(
label: '测试点击坐标',
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const TouchCoordinatePage(),
),
),
),
const SizedBox(height: 12),
DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(_status),
),
),
SizedBox(
height: _homeBannerAd == null ? 72 : 72 + _homeBannerHeight,
),
],
),
if (_homeBannerAd != null)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SafeArea(
top: false,
left: false,
right: false,
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: const <BoxShadow>[
BoxShadow(
blurRadius: 18,
color: Color(0x33000000),
offset: Offset(0, 6),
),
],
),
child: SizedBox(
width: double.infinity,
height: _homeBannerHeight,
child: MagicBannerAdView(adObject: _homeBannerAd!),
),
),
),
),
],
),
);
}
}
enum MagicAdDemoKind {
splash,
rewarded,
landscapeRewarded,
feed,
selfRenderFeed,
}
class MagicAdShowPage extends StatefulWidget {
const MagicAdShowPage({required this.kind, required this.tagId, super.key});
final MagicAdDemoKind kind;
final String tagId;
@override
State<MagicAdShowPage> createState() => _MagicAdShowPageState();
}
class _MagicAdShowPageState extends State<MagicAdShowPage> {
static const double _embeddedFeedHorizontalPadding = 16;
StreamSubscription<MagicBaseResponse>? _adSubscription;
MagicAdObject? _adObject;
double? _feedHeight;
String _status = '广告请求中...';
@override
void initState() {
super.initState();
_adSubscription = _eventStreamForKind(widget.kind).listen(_handleAdEvent);
WidgetsBinding.instance.addPostFrameCallback((_) => _loadAd());
}
@override
void dispose() {
_adSubscription?.cancel();
_adObject?.destroy();
super.dispose();
}
Future<void> _handleAdEvent(MagicBaseResponse event) async {
final MagicAdObject? eventObject = event.adObject;
final MagicAdObject? pageObject = _adObject;
if (eventObject == null || pageObject == null) return;
if (eventObject.objectID != pageObject.objectID) return;
if (event is MagicFeedResponse &&
event.feedStatus == MagicFeedStatus.loaded) {
_feedHeight = event.realHeight;
if (mounted) setState(() {});
}
if (event is MagicSelfRenderFeedResponse &&
event.selfRenderFeedStatus == MagicSelfRenderFeedStatus.loaded) {
_feedHeight = event.data?.realHeight;
if (mounted) setState(() {});
}
if (!mounted) return;
if (!_isTerminalEvent(event)) return;
_adObject = null;
if (mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
}
bool _isTerminalEvent(MagicBaseResponse event) {
if (_isEmbeddedFeed) return event.isClosed;
return event.isClosed ||
(event is MagicRewardedResponse &&
event.rewardedStatus == MagicRewardedStatus.finished);
}
bool get _isEmbeddedFeed =>
widget.kind == MagicAdDemoKind.feed ||
widget.kind == MagicAdDemoKind.selfRenderFeed;
Future<void> _loadAd() async {
setState(() => _status = '${_title()} 请求中...');
try {
final double screenWidth = MediaQuery.sizeOf(context).width;
final double embeddedFeedWidth = screenWidth -
_embeddedFeedHorizontalPadding -
_embeddedFeedHorizontalPadding;
await MagicInitManger.initMagicSDK(appId: DemoIds.defaultAppId);
final MagicAdObject? adObject = switch (widget.kind) {
MagicAdDemoKind.splash => await MagicSplashManager.loadSplash(
placementID: widget.tagId,
extraMap: <String, Object?>{
MagicSplashManager.tolerateTimeoutKey(): '3',
},
),
MagicAdDemoKind.rewarded =>
await MagicRewardedManager.loadRewardedVideo(
placementID: widget.tagId,
extraMap: <String, Object?>{
MagicRewardedManager.videoMuteKey(): '1'
},
),
MagicAdDemoKind.landscapeRewarded =>
await MagicRewardedManager.loadRewardedVideo(
placementID: widget.tagId,
extraMap: <String, Object?>{
MagicRewardedManager.videoMuteKey(): '1'
},
),
MagicAdDemoKind.feed => await MagicFeedManager.loadFeedAd(
placementID: widget.tagId,
width: embeddedFeedWidth,
extraMap: <String, Object?>{MagicFeedManager.autoPlayKey(): '1'},
),
MagicAdDemoKind.selfRenderFeed =>
await MagicFeedManager.loadSelfRenderFeedAd(
placementID: widget.tagId,
width: embeddedFeedWidth,
),
};
if (adObject == null) return;
if (!mounted) {
await adObject.destroy();
return;
}
_adObject = adObject;
if (adObject is MagicFeedAdObject) {
_feedHeight = await adObject.realHeight() ?? _feedHeight;
} else if (adObject is MagicSelfRenderFeedAdObject) {
_feedHeight = await adObject.realHeight() ?? _feedHeight;
}
if (_isEmbeddedFeed) {
// Feed ads are embedded by MagicFeedAdView/MagicSelfRenderFeedAdView.
} else if (adObject is MagicSplashAdObject) {
await adObject.show();
} else if (adObject is MagicRewardedAdObject) {
await adObject.show();
} else if (adObject is MagicInterstitialAdObject) {
await adObject.show();
}
if (mounted) {
setState(
() => _status = '${_title()}: ${_isEmbeddedFeed ? '广告已加载' : '广告已展示'}',
);
}
} on PlatformException catch (error) {
if (mounted) {
setState(
() => _status = '${_title()} failed: ${error.code} ${error.message}',
);
}
} catch (error) {
if (mounted) {
setState(() => _status = '${_title()} failed: $error');
}
}
}
String _title() {
return switch (widget.kind) {
MagicAdDemoKind.splash => '开屏-大-图片',
MagicAdDemoKind.rewarded => '激励视频',
MagicAdDemoKind.landscapeRewarded => '激励横视频',
MagicAdDemoKind.feed => '瀑布流广告',
MagicAdDemoKind.selfRenderFeed => '瀑布流自渲染广告',
};
}
@override
Widget build(BuildContext context) {
final MagicAdObject? adObject = _adObject;
return Scaffold(
appBar: AppBar(title: Text(_title())),
body: _isEmbeddedFeed
? ListView.separated(
padding: const EdgeInsets.all(_embeddedFeedHorizontalPadding),
itemBuilder: (context, index) {
if (index == 3) {
if (adObject == null) {
return _InfoCard(
title: '广告位',
value: '加载中',
helperText: '广告加载完成后会自动插入这里',
);
}
return SizedBox(
height: _feedHeight ?? _defaultEmbeddedHeight,
child: switch (adObject) {
MagicFeedAdObject feedAd =>
MagicFeedAdView(adObject: feedAd),
MagicSelfRenderFeedAdObject selfRenderAd =>
MagicSelfRenderFeedAdView(adObject: selfRenderAd),
_ => const SizedBox.shrink(),
},
);
}
return _InfoCard(
title: '列表内容 ${index + 1}',
value: index == 0 ? _status : 'Flutter 列表 item',
helperText: index == 2 ? '下一项是嵌入的原生广告 PlatformView' : '',
);
},
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemCount: 8,
)
: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text(_status, textAlign: TextAlign.center),
const SizedBox(height: 8),
Text(
'Tag ID: ${widget.tagId}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
);
}
double get _defaultEmbeddedHeight => 220;
}
Stream<MagicBaseResponse> _eventStreamForKind(MagicAdDemoKind kind) {
return switch (kind) {
MagicAdDemoKind.splash => MagicListenerManager.splashEventHandler,
MagicAdDemoKind.rewarded ||
MagicAdDemoKind.landscapeRewarded =>
MagicListenerManager.rewardedVideoEventHandler,
MagicAdDemoKind.feed => MagicListenerManager.feedEventHandler,
MagicAdDemoKind.selfRenderFeed =>
MagicListenerManager.selfRenderFeedEventHandler,
};
}
class TouchCoordinatePage extends StatefulWidget {
const TouchCoordinatePage({super.key});
@override
State<TouchCoordinatePage> createState() => _TouchCoordinatePageState();
}
class _TouchCoordinatePageState extends State<TouchCoordinatePage> {
Offset? _down;
Offset? _up;
double get _delta {
final Offset? down = _down;
final Offset? up = _up;
if (down == null || up == null) return 0;
return (up - down).distance;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('测试点击坐标')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('Down: ${_down ?? '-'}'),
Text('Up: ${_up ?? '-'}'),
Text('Delta: ${_delta.toStringAsFixed(2)}'),
const SizedBox(height: 16),
Expanded(
child: GestureDetector(
onTapDown: (details) {
setState(() => _down = details.localPosition);
},
onTapUp: (details) {
setState(() => _up = details.localPosition);
final String message = _delta <= 15 ? '触发点击' : '移动距离过大,未触发点击';
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
},
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.primaryContainer,
),
child: const Center(child: Text('点击/滑动测试区域')),
),
),
),
],
),
),
);
}
}
class _DemoButton extends StatelessWidget {
const _DemoButton({required this.label, required this.onPressed});
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SizedBox(
width: double.infinity,
height: 44,
child: FilledButton(onPressed: onPressed, child: Text(label)),
),
);
}
}
class _InfoCard extends StatelessWidget {
const _InfoCard({
required this.title,
required this.value,
required this.helperText,
});
final String title;
final String value;
final String helperText;
@override
Widget build(BuildContext context) {
final ColorScheme colors = Theme.of(context).colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: colors.outlineVariant),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(title, style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 4),
Text(value, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 4),
Text(helperText, style: Theme.of(context).textTheme.bodySmall),
],
),
),
);
}
}