Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visibility detector workaround for android impression issue #610

Merged
merged 5 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/google_mobile_ads/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.0.1
* Bug fix for [issue 580](https://github.com/googleads/googleads-mobile-flutter/issues/580).
Adds a workaround on Android to wait for the ad widget to become visible
before attaching the platform view.

## 2.0.0
* Updates GMA Android dependency to 21.0.0 and iOS to 9.6.0
* Removes `credentials` from `AdapterResponseInfo`, which is replaced with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/** Constants used in the plugin. */
public class Constants {
/** Version request agent. Should be bumped alongside plugin versions. */
public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-2.0.0";
public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-2.0.1";

static final String ERROR_CODE_UNEXPECTED_AD_TYPE = "unexpected_ad_type";
}
2 changes: 1 addition & 1 deletion packages/google_mobile_ads/ios/Classes/FLTConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
// limitations under the License.

/** Versioned request agent string. */
#define FLT_REQUEST_AGENT_VERSIONED @"Flutter-GMA-2.0.0"
#define FLT_REQUEST_AGENT_VERSIONED @"Flutter-GMA-2.0.1"
69 changes: 47 additions & 22 deletions packages/google_mobile_ads/lib/src/ad_containers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:visibility_detector/visibility_detector.dart';

import 'ad_instance_manager.dart';
import 'ad_listeners.dart';
Expand Down Expand Up @@ -590,6 +591,7 @@ class AdWidget extends StatefulWidget {
class _AdWidgetState extends State<AdWidget> {
bool _adIdAlreadyMounted = false;
bool _adLoadNotCalled = false;
bool _firstVisibleOccurred = false;

@override
void initState() {
Expand Down Expand Up @@ -635,28 +637,51 @@ class _AdWidgetState extends State<AdWidget> {
]);
}
if (defaultTargetPlatform == TargetPlatform.android) {
return PlatformViewLink(
viewType: '${instanceManager.channel.name}/ad_widget',
surfaceFactory:
(BuildContext context, PlatformViewController controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: '${instanceManager.channel.name}/ad_widget',
layoutDirection: TextDirection.ltr,
creationParams: instanceManager.adIdFor(widget.ad),
creationParamsCodec: StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
// Do not attach the platform view widget until it will actually become
// visible. This is a workaround for
// https://github.com/googleads/googleads-mobile-flutter/issues/580,
// where impressions are erroneously fired due to how platform views are
// rendered.
// TODO (jjliu15): Remove this after the flutter issue is resolved.
if (_firstVisibleOccurred) {
return PlatformViewLink(
viewType: '${instanceManager.channel.name}/ad_widget',
surfaceFactory:
(BuildContext context, PlatformViewController controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <
Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: '${instanceManager.channel.name}/ad_widget',
layoutDirection: TextDirection.ltr,
creationParams: instanceManager.adIdFor(widget.ad),
creationParamsCodec: StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
} else {
final adId = instanceManager.adIdFor(widget.ad);
return VisibilityDetector(
key: Key('android-platform-view-$adId'),
onVisibilityChanged: (visibilityInfo) {
if (!_firstVisibleOccurred &&
visibilityInfo.visibleFraction > 0.01) {
setState(() {
_firstVisibleOccurred = true;
});
}
},
child: Container(),
);
}
}

return UiKitView(
Expand Down
1 change: 1 addition & 0 deletions packages/google_mobile_ads/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies:
meta: ^1.0.4
flutter:
sdk: flutter
visibility_detector: ^0.3.3

dev_dependencies:
pedantic: ^1.11.0
Expand Down
93 changes: 71 additions & 22 deletions packages/google_mobile_ads/test/ad_containers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/src/ad_instance_manager.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart';
import 'package:visibility_detector/visibility_detector.dart';

// ignore_for_file: deprecated_member_use_from_same_package
void main() {
Expand Down Expand Up @@ -282,7 +282,8 @@ void main() {
expect(instanceManager.adFor(0), isNotNull);
});

testWidgets('build ad widget', (WidgetTester tester) async {
testWidgets('build ad widget iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
final NativeAd native = NativeAd(
adUnitId: 'test-ad-unit',
factoryId: '0',
Expand All @@ -293,41 +294,89 @@ void main() {
await native.load();

await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
AdWidget widget = AdWidget(ad: native);
Widget buildWidget = widget.createElement().build();
expect(buildWidget, isA<PlatformViewLink>());
return widget;
},
MaterialApp(
home: Material(
child: SingleChildScrollView(
child: Column(
key: UniqueKey(),
children: [
SizedBox.fromSize(size: Size(200, 1000)),
Container(
height: 200,
width: 200,
child: AdWidget(ad: native),
),
],
),
),
),
),
);
await tester.pumpAndSettle();

final uiKitView = tester.widget(find.byType(UiKitView));
expect(uiKitView, isNotNull);

await native.dispose();
debugDefaultTargetPlatformOverride = null;
});

testWidgets('build ad widget', (WidgetTester tester) async {
final NativeAd native = NativeAd(
testWidgets('Build ad widget Android', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
// Create a loaded ad
final ad = NativeAd(
adUnitId: 'test-ad-unit',
factoryId: '0',
listener: NativeAdListener(),
request: AdRequest(),
);
await ad.load();

await native.load();

// Render ad in a scrolling view
VisibilityDetectorController.instance.updateInterval = Duration.zero;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
AdWidget widget = AdWidget(ad: native);
Widget buildWidget = widget.createElement().build();
expect(buildWidget, isA<PlatformViewLink>());
return widget;
},
MaterialApp(
home: Material(
child: SingleChildScrollView(
child: Column(
key: UniqueKey(),
children: [
SizedBox.fromSize(size: Size(200, 1000)),
Container(
height: 200,
width: 200,
child: AdWidget(ad: ad),
),
],
),
),
),
),
);

await native.dispose();
await tester.pumpAndSettle();

// On initial render, VisibilityRender should be in the UI
final visibilityDetectorWidget =
tester.widget(find.byKey(Key('android-platform-view-0')));
expect(visibilityDetectorWidget, isNotNull);
expect(visibilityDetectorWidget, isA<VisibilityDetector>());
final platformViewLinks =
tester.widgetList(find.byType(PlatformViewLink));
expect(platformViewLinks.isEmpty, true);

// Drag the ad widget into view
await tester.drag(find.byType(SingleChildScrollView), Offset(0.0, -1000));
await tester.pumpAndSettle();

// PlatformViewLink should now be present instead of VisibilityDetector
final detectors = tester.widgetList(find.byType(VisibilityDetector));
expect(detectors.isEmpty, true);
final platformViewLink = tester.widget(find.byType(PlatformViewLink));
expect(platformViewLink, isNotNull);

// Reset platform override
await ad.dispose();
debugDefaultTargetPlatformOverride = null;
});

testWidgets('warns when ad has not been loaded',
Expand Down