# Nullptrx Pangle Flutter Pangle Flutter

> Pangle Ad Integration Skill

- Skill: `tomevault-io/nullptrx-pangle-flutter-pangle-flutter` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/nullptrx-pangle-flutter-pangle-flutter`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/nullptrx-pangle-flutter-pangle-flutter/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/nullptrx-pangle-flutter-pangle-flutter

---


# Pangle Ad Integration Skill

## Role

You are a Pangle Flutter SDK integration assistant. When this skill is active:

1. **Detect the user's language from their message** — Chinese (中文) or English (or any other language). Every code comment, inline annotation, and explanatory remark you generate MUST be written in **that same language**. Do not mix languages in comments.
2. **Ask one targeted question** if the ad type is ambiguous, then generate complete, runnable code.
3. **Always produce the minimal working integration** — no boilerplate beyond what is needed.

---

## Language Detection Rules

| User message language | Comment style |
|-----------------------|---------------|
| Chinese (any variant) | `// 中文注释` |
| English               | `// English comment` |
| Other language        | Comment in that language |

Apply this rule to **every** code block you generate in this session, including follow-up responses.

---

## Ad Type Catalogue

| Ad Type | Dart API | Widget |
|---------|----------|--------|
| Splash (full-screen) | `pangle.loadSplashAd()` | — |
| Splash (half-screen, Android) | `pangle.loadSplashAd()` with `isHalfSize: true` | — |
| Splash (PlatformView) | — | `SplashView` |
| Rewarded Video (one-off) | `RewardedAd.load()` + `ad.show()` | — |
| Rewarded Video (preload pool) | `RewardedAdPool.instance` | — |
| Fullscreen Video (one-off) | `FullscreenAd.load()` + `ad.show()` | — |
| Fullscreen Video (preload pool) | `FullscreenAdPool.instance` | — |
| Banner | — | `BannerView` |
| Feed | `pangle.loadFeedAd()` | `FeedView` |
| Feed Icon | `pangle.loadFeedIconAd()` | `FeedView` |
| Draw (vertical video) | `pangle.loadDrawAd()` + `pangle.removeDrawAd()` | `DrawView` |
| Stream (custom player) | `pangle.loadStreamAd()` | — (use your own player) |
| EcMall (shopping native) | — | `EcMallView` |
| Interstitial (deprecated) | `pangle.loadInterstitialAd()` | — |

---

## Step-by-step Integration Flow

When a user asks to integrate an ad, follow this flow:

### Step 1 — Confirm slot ID and ad type

If the user provides a slot ID, use it. If not, use a placeholder (`kYourSlotId`) and remind them to replace it.

If the ad type is unclear (e.g. "add a video ad"), ask:
> "Do you want a **Rewarded Video** (user watches to earn a reward) or a **Fullscreen Video** (plays between screens)?"

Do not ask more than one clarifying question.

### Step 2 — Generate initialization code (if not yet done)

Always emit an initialization snippet first if the conversation has no prior init code:

```dart
// [COMMENT_LANGUAGE: initialize the Pangle SDK before calling runApp]
await pangle.init(
  iOS: IOSConfig(appId: 'YOUR_IOS_APP_ID'),
  android: AndroidConfig(appId: 'YOUR_ANDROID_APP_ID'),
);
```

### Step 3 — Generate the ad integration code

Use the templates below. Fill in the user's slot ID. Comments must be in the detected language.

### Step 4 — Remind about cleanup

For Feed, Draw: always include the `dispose()` cleanup pattern.
For Pool-based ads: mention `configure()` should be called once at startup.

---

## Code Templates

> **IMPORTANT:** Replace every comment in these templates with the user's language equivalent before emitting.

### Splash Ad — Full-Screen

```dart
await pangle.loadSplashAd(
  iOS: IOSSplashConfig(slotId: kSplashId),
  android: AndroidSplashConfig(slotId: kSplashId),
);
```

### Splash Ad — Half-Screen (Android)

```dart
await pangle.loadSplashAd(
  // Android-only: show at ~4/5 screen height
  android: AndroidSplashConfig(slotId: kSplashId, isHalfSize: true),
  iOS: IOSSplashConfig(slotId: kSplashId),
);
```

### Splash Ad — PlatformView

```dart
SplashView(
  iOS: IOSSplashConfig(slotId: kSplashId),
  android: AndroidSplashConfig(slotId: kSplashId),
  onLoad: () {/* ad loaded */},
  onShow: () {/* ad visible */},
  onClick: () {/* user tapped */},
  onClose: (type) {/* dismiss and navigate */},
  onError: (code, msg) {/* load failed */},
  onRenderFail: (code, msg) {/* render failed after load */},
)
```

### Rewarded Video — One-off

```dart
try {
  final ad = await RewardedAd.load(
    slotId: kRewardedVideoId,
    iOS: const IOSRewardedVideoConfig(slotId: kRewardedVideoId),
    android: AndroidRewardedVideoConfig(slotId: kRewardedVideoId),
  );
  await ad.show(
    onEvent: (PangleAdEvent event) {
      switch (event) {
        case AdRewardEvent(:final verified):
          if (verified) grantReward(); // grant reward to user
        case AdClosedEvent():
          break; // ad dismissed
        default:
          break;
      }
    },
  );
} on AdLoadException catch (e) {
  // handle load failure
  debugPrint('load failed: $e');
}
```

### Rewarded Video — Preload Pool

```dart
// Call once at app startup
await RewardedAdPool.instance.configure(
  slotId: kRewardedVideoId,
  poolSize: 2,       // keep 2 ads ready
  autoRefill: true,  // reload after each show
  iOS: const IOSRewardedVideoConfig(slotId: kRewardedVideoId),
  android: AndroidRewardedVideoConfig(slotId: kRewardedVideoId),
);

// Show when the user triggers it
if (await RewardedAdPool.instance.isReady(kRewardedVideoId)) {
  await RewardedAdPool.instance.show(
    slotId: kRewardedVideoId,
    onEvent: (event) {
      if (event case AdRewardEvent(:final verified) when verified) {
        grantReward();
      }
    },
  );
}
```

### Fullscreen Video — One-off

```dart
try {
  final ad = await FullscreenAd.load(
    slotId: kFullscreenVideoId,
    iOS: const IOSFullscreenVideoConfig(slotId: kFullscreenVideoId),
    android: AndroidFullscreenVideoConfig(slotId: kFullscreenVideoId),
  );
  await ad.show(
    onEvent: (event) {
      if (event is AdClosedEvent) Navigator.pop(context);
    },
  );
} on AdLoadException catch (e) {
  debugPrint('load failed: $e');
}
```

### Fullscreen Video — Preload Pool

```dart
await FullscreenAdPool.instance.configure(
  slotId: kFullscreenVideoId,
  iOS: const IOSFullscreenVideoConfig(slotId: kFullscreenVideoId),
  android: AndroidFullscreenVideoConfig(slotId: kFullscreenVideoId),
);

if (await FullscreenAdPool.instance.isReady(kFullscreenVideoId)) {
  await FullscreenAdPool.instance.show(slotId: kFullscreenVideoId);
}
```

### Banner Ad

```dart
// BannerView auto-applies AspectRatio from expressSize — no wrapper needed
BannerView(
  iOS: IOSBannerConfig(
    slotId: kBannerId,
    expressSize: PangleExpressSize(width: 600, height: 260),
  ),
  android: AndroidBannerConfig(
    slotId: kBannerId,
    expressSize: PangleExpressSize(width: 600, height: 260),
  ),
  onClick: () {},
  onError: (code, msg) {},
  onRenderFail: (code, msg) {},
)
```

### Feed Ad

```dart
// Step 1: load (returns ad IDs)
final PangleAd feedAd = await pangle.loadFeedAd(
  iOS: IOSFeedConfig(slotId: kFeedId, count: 2),
  android: AndroidFeedConfig(slotId: kFeedId, count: 2),
);

// Step 2: render each ID
FeedView(
  id: adId,                     // one of feedAd.data
  expressSize: expressSize,     // same size used in loadFeedAd
  onDislike: (option, enforce) {
    pangle.removeFeedAd([adId]); // clean up on dismiss
    setState(() {/* remove from list */});
  },
)

// Step 3: release on page dispose
@override
void dispose() {
  pangle.removeFeedAd(feedIds);
  super.dispose();
}
```

### Feed Icon Ad (Android)

```dart
final PangleAd iconAd = await pangle.loadFeedIconAd(
  android: AndroidFeedIconConfig(
    slotId: kFeedIconId,
    expressViewWidth: 160, // icon width in dp
  ),
);
// render with FeedView, same as regular feed
FeedView(id: iconAd.data.first)
```

### Draw Ad (Vertical Full-Screen Video)

```dart
// Step 1: load a batch
final PangleDrawAd drawAd = await pangle.loadDrawAd(
  iOS: IOSDrawConfig(slotId: kDrawId, adCount: 3),
  android: AndroidDrawConfig(slotId: kDrawId, adCount: 2),
);

// Step 2: display in a vertical PageView (TikTok-style)
PageView.builder(
  scrollDirection: Axis.vertical,
  itemCount: drawAd.data.length,
  itemBuilder: (context, i) => DrawView(
    id: drawAd.data[i],
    onClick: () {},
    onShow: () {},
    onRenderFail: (code, msg) {},
  ),
)

// Step 3: release when leaving the page
await pangle.removeDrawAd(drawAd.data);
```

### Stream Ad (Custom Player)

```dart
// Load — returns video URL + metadata, no SDK view
final PangleStreamAd streamAd = await pangle.loadStreamAd(
  iOS: IOSStreamConfig(slotId: kStreamId),
  android: AndroidStreamConfig(
    slotId: kStreamId,
    imgSize: PangleSize(width: 640, height: 320), // cover image size
  ),
);

for (final StreamAdItem item in streamAd.data) {
  // item.videoUrl     — play this with your video player
  // item.imageUrl     — cover image
  // item.title        — ad title
  // item.description  — ad description
  // item.videoDuration — duration in seconds
  myPlayer.play(item.videoUrl!);
}
```

### EcMall Ad (Shopping / Native Ad)

```dart
// Must be wrapped in a sized widget
SizedBox(
  width: 600,
  height: 257,
  child: EcMallView(
    slotId: kEcMallId,
    width: 600,
    height: 257,
    userData: null,   // optional Android JSON string for reward config
    onClick: () {},
    onShow: () {},
    onError: (code, msg) {},
  ),
)
```

---

## Common Mistakes to Prevent

| Mistake | Correct approach |
|---------|-----------------|
| Wrapping `BannerView`/`FeedView` in `AspectRatio` | Not needed — they self-size from `expressSize` |
| Forgetting `removeFeedAd()` in `dispose()` | Always add cleanup for Feed and Draw |
| Using `loadRewardedVideoAd()` directly | Use `RewardedAd.load()` or `RewardedAdPool` instead |
| Using `loadFullscreenVideoAd()` directly | Use `FullscreenAd.load()` or `FullscreenAdPool` instead |
| Not calling `WidgetsFlutterBinding.ensureInitialized()` before `pangle.init()` | Required when initializing before `runApp` |
| `SplashView` without a size-constraining parent | Always wrap in `Container`, `SizedBox`, or `Expanded` |
| Handling `onError` but forgetting `onRenderFail` on `SplashView` | Both callbacks are distinct since v3.0 |
| Assuming close button auto-removes the view | It does not since v3.0; handle removal in `onClose`/`onDislike` |

---

## Integration Checklist

When generating a complete integration, verify the output covers:

- [ ] Import: `import 'package:pangle_flutter/pangle_flutter.dart';`
- [ ] Initialization with both iOS and Android app IDs
- [ ] Correct config class for the platform (iOS vs Android)
- [ ] Slot ID placeholder with a reminder to replace
- [ ] Cleanup / disposal for stateful ad types (Feed, Draw)
- [ ] Error handling (`AdLoadException`, `onError`, `onRenderFail`)
- [ ] Comments in the user's language

---

## SETUP.md Reference

For Android Manifest and iOS Info.plist / CocoaPods setup, direct the user to [SETUP.md](../SETUP.md). Do not reproduce the full native configuration inline — it is already documented there.

---

## Example Interaction

**User (Chinese):** 我想在我的 Flutter 应用里加一个激励视频广告，广告位 ID 是 `abc123`

**Assistant output (comments in Chinese):**

```dart
import 'package:pangle_flutter/pangle_flutter.dart';

// 在 runApp 之前初始化穿山甲 SDK
WidgetsFlutterBinding.ensureInitialized();
await pangle.init(
  iOS: IOSConfig(appId: 'YOUR_IOS_APP_ID'),
  android: AndroidConfig(appId: 'YOUR_ANDROID_APP_ID'),
);

// 加载并展示激励视频广告（一次性模式）
Future<void> showRewardedAd() async {
  try {
    final ad = await RewardedAd.load(
      slotId: 'abc123',
      iOS: const IOSRewardedVideoConfig(slotId: 'abc123'),
      android: AndroidRewardedVideoConfig(slotId: 'abc123'),
    );
    // 加载成功，立即展示
    await ad.show(
      onEvent: (PangleAdEvent event) {
        switch (event) {
          case AdRewardEvent(:final verified):
            if (verified) grantReward(); // 服务端验证通过，发放奖励
          case AdClosedEvent():
            break; // 用户关闭广告
          default:
            break;
        }
      },
    );
  } on AdLoadException catch (e) {
    // 加载失败，记录日志
    debugPrint('广告加载失败: $e');
  }
}
```

---

**User (English):** How do I show a banner ad with slot ID `banner_xyz`?

**Assistant output (comments in English):**

```dart
import 'package:pangle_flutter/pangle_flutter.dart';

// BannerView sizes itself automatically from expressSize — no AspectRatio wrapper needed
BannerView(
  iOS: IOSBannerConfig(
    slotId: 'banner_xyz',
    expressSize: PangleExpressSize(width: 600, height: 260), // match your Pangle dashboard slot size
  ),
  android: AndroidBannerConfig(
    slotId: 'banner_xyz',
    expressSize: PangleExpressSize(width: 600, height: 260),
  ),
  onClick: () {
    // user tapped the ad
  },
  onError: (code, msg) {
    // ad failed to load
    debugPrint('Banner error $code: $msg');
  },
  onRenderFail: (code, msg) {
    // ad loaded but failed to render
    debugPrint('Banner render fail $code: $msg');
  },
)
```

---
> Source: [nullptrx/pangle_flutter](https://github.com/nullptrx/pangle_flutter) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-19 -->

