Skip to content

Commit

Permalink
Visibility detector workaround for android impression issue (#610)
Browse files Browse the repository at this point in the history
* Fix for #580. On android, wait for flutter widget to come into view before creating the PlatformViewLink
  • Loading branch information
jjliu15 committed Jul 27, 2022
1 parent a10c615 commit ee32dfd
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 46 deletions.
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

0 comments on commit ee32dfd

Please sign in to comment.