Skip to content

Commit

Permalink
🔧 refactor(main.dart): import 'dart:async' and 'package:xandr/load_mo…
Browse files Browse the repository at this point in the history
…de.dart' to support asynchronous operations and load mode functionality

✨ feat(main.dart): add ScrollController and StreamController to manage scroll events and check if ad is in viewport
🔧 refactor(main.dart): add listener to _scrollController to add its position to _checkIfAdIsInViewport stream
✨ feat(main.dart): add more Text widgets and a new AdBanner with loadMode set to LoadMode.whenInViewport to demonstrate ad loading when in viewport
🔧 refactor(ad_banner.dart): import 'dart:async' and 'package:xandr/load_mode.dart' to support asynchronous operations and load mode functionality
✨ feat(ad_banner.dart): add loadMode parameter to AdBanner and _HostAdBannerView to determine when the ad is loaded
✨ feat(ad_banner.dart): add loadAd method to trigger ad loading via MethodChannel
✨ feat(ad_banner.dart): add _checkViewport method to check if the ad is in the viewport and load it if it is
🔧 refactor(ad_banner.dart): call loadAd in initState if loadMode is LoadWhenCreated
🔧 refactor(ad_banner.dart): call _checkViewport in initState if loadMode is WhenInViewport
🔧 refactor(ad_banner.dart): add onDoneLoading callback to _HostAdBannerView to update _loading and _loaded states when ad loading is done
🔧 refactor(ad_banner.dart): call loadAd in _HostAdBannerView's onPlatformViewCreated if loadMode is LoadWhenCreated
🔧 refactor(ad_banner.dart): add loadMode to creationParams to pass it to the native code
🔧 refactor(ad_banner.dart): add widgetId to _HostAdBannerView to pass it to the native code
🔧 refactor(ad_banner.dart): complete widgetId in _HostAdBannerView's onPlatformViewCreated if it's not completed yet
🔧 refactor(ad_banner.dart): call onDoneLoading in _HostAdBannerView's onPlatformViewCreated when ad loading is done
🔧 refactor(ad_banner.dart): call loadAd in _HostAdBannerView's onPlatformViewCreated if loadMode is LoadWhenCreated
🔧 refactor(ad_banner.dart): add loadMode to _HostAdBannerView's creationParams to pass it to the native code
🔧 refactor(ad_banner.dart): add loadMode
  • Loading branch information
thekorn committed Feb 18, 2024
1 parent 97cbe31 commit b4e72c4
Show file tree
Hide file tree
Showing 15 changed files with 346 additions and 18 deletions.
68 changes: 68 additions & 0 deletions packages/xandr/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:xandr/ad_banner.dart';
import 'package:xandr/ad_size.dart';
import 'package:xandr/load_mode.dart';
import 'package:xandr/xandr.dart';
import 'package:xandr/xandr_builder.dart';

Expand Down Expand Up @@ -34,12 +37,25 @@ class XandrExample extends StatefulWidget {

class _XandrExampleState extends State<XandrExample> {
late final XandrController _controller;
final ScrollController _scrollController = ScrollController();
final StreamController<ScrollPosition> _checkIfAdIsInViewport =
StreamController();

@override
void dispose() {
_scrollController.dispose();
_checkIfAdIsInViewport.close();
super.dispose();
}

@override
void initState() {
super.initState();

_controller = XandrController();
_scrollController.addListener(() {
_checkIfAdIsInViewport.add(_scrollController.position);
});
}

@override
Expand All @@ -57,6 +73,7 @@ class _XandrExampleState extends State<XandrExample> {
if (snapshot.hasData) {
debugPrint('Xandr SDK initialized, success=${snapshot.hasData}');
return SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
const Text(
Expand Down Expand Up @@ -114,6 +131,31 @@ class _XandrExampleState extends State<XandrExample> {
//customKeywords: useDemoAds,
resizeWhenLoaded: true,
),
const Text(
'Lorem Ipsum is simply dummy text of the printing and '
'typesetting industry. Lorem Ipsum has been the boo '
'standard dummy text ever since the 1500s, when an aha '
'printer took a galley of type and scrambled it to v'),
const Text(
'Lorem Ipsum is simply dummy text of the printing and '
'typesetting industry. Lorem Ipsum has been the boo '
'standard dummy text ever since the 1500s, when an aha '
'printer took a galley of type and scrambled it to v'),
const Text(
'Lorem Ipsum is simply dummy text of the printing and '
'typesetting industry. Lorem Ipsum has been the boo '
'standard dummy text ever since the 1500s, when an aha '
'printer took a galley of type and scrambled it to v'),
const Text(
'Lorem Ipsum is simply dummy text of the printing and '
'typesetting industry. Lorem Ipsum has been the boo '
'standard dummy text ever since the 1500s, when an aha '
'printer took a galley of type and scrambled it to v'),
const Text(
'Lorem Ipsum is simply dummy text of the printing and '
'typesetting industry. Lorem Ipsum has been the boo '
'standard dummy text ever since the 1500s, when an aha '
'printer took a galley of type and scrambled it to v'),
const Text(
'Lorem Ipsum is simply dummy text of the printing and '
'typesetting industry. Lorem Ipsum has been the boo '
Expand Down Expand Up @@ -153,6 +195,32 @@ class _XandrExampleState extends State<XandrExample> {
'typesetting industry. Lorem Ipsum has been the boo '
'standard dummy text ever since the 1500s, when an aha '
'printer took a galley of type and scrambled it to g'),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Align(
alignment: Alignment.topLeft,
child: Text(
'load when in viewport:',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
AdBanner(
controller: _controller,
//placementID: '17058950',
inventoryCode: 'bunte_webdesktop_home_homepage_hor_1',
adSizes: const [
AdSize(1, 1),
AdSize(728, 90),
], //[AdSize(300, 250)],
resizeAdToFitContainer: true,
loadMode: LoadMode.whenInViewport(
_checkIfAdIsInViewport.stream,
-100,
),
),
],
),
);
Expand Down
84 changes: 81 additions & 3 deletions packages/xandr/lib/ad_banner.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import 'dart:async';

import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:xandr/ad_size.dart';
import 'package:xandr/load_mode.dart';
import 'package:xandr/xandr.dart';
import 'package:xandr_android/xandr_android.dart';

Expand All @@ -25,6 +29,7 @@ class AdBanner extends StatefulWidget {
this.resizeAdToFitContainer = false,
this.loadsInBackground,
this.shouldServePSAs,
LoadMode? loadMode,
double? width,
double? height,
}) : assert(adSizes.isNotEmpty, 'adSizes must not be empty'),
Expand All @@ -33,7 +38,8 @@ class AdBanner extends StatefulWidget {
'placementID or inventoryCode must not be null',
),
width = width ?? adSizes.first.width.toDouble(),
height = height ?? adSizes.first.height.toDouble();
height = height ?? adSizes.first.height.toDouble(),
loadMode = loadMode ?? LoadMode.whenCreated();

/// The placement ID for the ad banner.
final String? placementID;
Expand Down Expand Up @@ -86,19 +92,64 @@ class AdBanner extends StatefulWidget {
/// They are not enabled by default.
final bool? shouldServePSAs;

/// The load mode for the ad banner, determines when the ad is loaded.
final LoadMode loadMode;

@override
State<AdBanner> createState() => _AdBannerState();
}

class _AdBannerState extends State<AdBanner> {
double _width = 1;
double _height = 1;
bool _loading = false;
bool _loaded = false;
final Completer<int> _widgetId = Completer();

@override
void initState() {
super.initState();
context.toString();
if (widget.loadMode is LoadWhenCreated) {
_loading = true;
} else if (widget.loadMode is WhenInViewport) {
(widget.loadMode as WhenInViewport).checkIfInViewport.listen((_) {
_checkViewport((widget.loadMode as WhenInViewport).pixelOffset);
});
}
_height = widget.height;
_width = widget.width;
super.initState();
}

/// trigger ad loading via [MethodChannel]
void loadAd() {
if (!_loaded && !_loading) {
setState(() {
_loading = true;
});
_widgetId.future.then((value) => widget.controller.loadAd(value));
}
}

/// function used only with the [WhenInViewport] loadMode
void _checkViewport(int pixelOffset) {
final object = context.findRenderObject();

if (object == null || !object.attached) {
return;
}

final viewport = RenderAbstractViewport.of(object);
final vpHeight = viewport.paintBounds.height;
final vpOffset = viewport.getOffsetToReveal(object, 0);

final deltaTop = vpOffset.offset - Scrollable.of(context).position.pixels;

if ((vpHeight - deltaTop) > pixelOffset) {
if (!_loading) {
loadAd();
}
}
}

void changeSize(double width, double height) {
Expand All @@ -108,6 +159,13 @@ class _AdBannerState extends State<AdBanner> {
});
}

void onDoneLoading({required bool success}) {
setState(() {
_loading = false;
_loaded = success;
});
}

@override
Widget build(BuildContext context) {
return SizedBox(
Expand All @@ -128,6 +186,9 @@ class _AdBannerState extends State<AdBanner> {
resizeAdToFitContainer: widget.resizeAdToFitContainer,
loadsInBackground: widget.loadsInBackground,
shouldServePSAs: widget.shouldServePSAs,
loadMode: widget.loadMode,
onDoneLoading: onDoneLoading,
widgetId: _widgetId,
delegate: BannerAdEventDelegate(
onBannerAdLoaded: (event) {
debugPrint('>>>> onBannerAdLoaded: $event');
Expand Down Expand Up @@ -163,6 +224,8 @@ enum ClickThroughAction {
}
}

typedef _DoneLoadingCallback = void Function({required bool success});

class _HostAdBannerView extends StatelessWidget {
_HostAdBannerView({
required String? placementID,
Expand All @@ -176,11 +239,15 @@ class _HostAdBannerView extends StatelessWidget {
required int layoutHeight,
required int layoutWidth,
required bool resizeAdToFitContainer,
required LoadMode loadMode,
required _DoneLoadingCallback onDoneLoading,
required this.widgetId,
ClickThroughAction? clickThroughAction,
bool? loadsInBackground,
bool? shouldServePSAs,
this.delegate,
}) : creationParams = <String, dynamic>{
}) : _onDoneLoading = onDoneLoading,
creationParams = <String, dynamic>{
'placementID': placementID,
'inventoryCode': inventoryCode,
'autoRefreshInterval': autoRefreshInterval.inMilliseconds,
Expand All @@ -191,6 +258,7 @@ class _HostAdBannerView extends StatelessWidget {
'layoutHeight': layoutHeight,
'layoutWidth': layoutWidth,
'resizeAdToFitContainer': resizeAdToFitContainer,
'loadWhenCreated': loadMode is LoadWhenCreated,
} {
if (clickThroughAction != null) {
creationParams['clickThroughAction'] = clickThroughAction.toString();
Expand All @@ -207,6 +275,8 @@ class _HostAdBannerView extends StatelessWidget {
final Map<String, dynamic> creationParams;
final XandrController controller;
final BannerAdEventDelegate? delegate;
final _DoneLoadingCallback _onDoneLoading;
final Completer<int> widgetId;

@override
Widget build(BuildContext context) {
Expand All @@ -215,14 +285,22 @@ class _HostAdBannerView extends StatelessWidget {
viewType: 'de.thekorn.xandr/ad_banner',
onPlatformViewCreated: (id) {
debugPrint('Created banner view: $id');

if (!widgetId.isCompleted) {
widgetId.complete(id);
}
controller.listen(id, (event) {
if (event is BannerAdLoadedEvent) {
_onDoneLoading(success: true);
delegate?.onBannerAdLoaded?.call(event);
} else if (event is BannerAdLoadedErrorEvent) {
_onDoneLoading(success: false);
delegate?.onBannerAdLoadedError?.call(event);
} else if (event is NativeBannerAdLoadedEvent) {
_onDoneLoading(success: true);
delegate?.onNativeBannerAdLoaded?.call(event);
} else if (event is NativeBannerAdLoadedErrorEvent) {
_onDoneLoading(success: false);
delegate?.onNativeBannerAdLoadedError?.call(event);
}
});
Expand Down
54 changes: 54 additions & 0 deletions packages/xandr/lib/load_mode.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';

/// Represents the load mode for a specific operation.
class LoadMode {
LoadMode._();

/// factory method to create a class [LoadWhenCreated]
factory LoadMode.whenCreated() = LoadWhenCreated;

/// factory method to create a class [WhenInViewport]
factory LoadMode.whenInViewport(
Stream<ScrollPosition> checkIfInViewport,
int pixelOffset,
) = WhenInViewport;
}

/// Use this class to automatically load the ad when it's created in the native
/// code.
///
/// Use the factory method [LoadMode.whenCreated] to create it.
class LoadWhenCreated extends LoadMode {
/// load the ad immediately when it's created.
LoadWhenCreated() : super._();
}

/// Use this class to when you want to asynchronously load the ad and
/// additionally check if it's in the viewport (+- pixelOffset).
///
/// [checkIfInViewport] pass null events, to trigger checking if the
/// BannerAdView is in the viewport (+- pixelOffset).
/// For example, the events could be generated by a [NotificationListener],
/// so that on every scroll, you would check if the BannerAdView is in the
/// viewport. [pixelOffset] number of pixels that is used to calculate when to
/// load the ad. NOTE: Bear in mind, that the BannerAdView should be added to
/// the widget tree. So dynamic ListViews with a builder method could not
/// catch -1000px offsets, because the BannerAdView is not rendered/created yet.
/// Example values:
/// If set to 0, the ad will load when the widget will enter the viewport.
/// If set to -100, the ad will load -100 px before appearing on the screen.
/// If set to 100, the ad will load when 100 px of the ad could be shown.
///
/// Use the factory method [LoadMode.whenInViewport] to create it.
class WhenInViewport extends LoadMode {
/// load ad when it's in the viewport
WhenInViewport(this.checkIfInViewport, int? pixelOffset)
: pixelOffset = pixelOffset ?? 0,
super._();

/// stream which should get new events when scrolling changes
final Stream<ScrollPosition> checkIfInViewport;

/// pixel offset determining when to load the ad
final int pixelOffset;
}
6 changes: 6 additions & 0 deletions packages/xandr/lib/xandr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class XandrController {
return _platform.init(memberId);
}

/// loads an ad.
Future<bool> loadAd(int widgetId) async {
debugPrint('loadAd');
return _platform.loadAd(widgetId);
}

/// loads an interstitial ad.
Future<bool> loadInterstitialAd({
String? placementID,
Expand Down
Loading

0 comments on commit b4e72c4

Please sign in to comment.