magic_ad_sdk 0.0.2 copy "magic_ad_sdk: ^0.0.2" to clipboard
magic_ad_sdk: ^0.0.2 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),
          ],
        ),
      ),
    );
  }
}
0
likes
130
points
163
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Magic Ads SDK Flutter plugin for Android and iOS.

Topics

#ads #advertising #flutter-plugin

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on magic_ad_sdk

Packages that implement magic_ad_sdk