Skip to content

Commit 025af72

Browse files
authored
Bug fix - Fix crash reusing banner and native ad objects on Android (#64)
* Only destroy native and banner ads from Flutter dispose(). * Add a page to the example app which only uses one ad object for native and banner ads
1 parent 826fb41 commit 025af72

File tree

13 files changed

+317
-97
lines changed

13 files changed

+317
-97
lines changed

packages/google_mobile_ads/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.11.0+3
2+
3+
* Fixes an [Android crash](https://github.com/googleads/googleads-mobile-flutter/issues/46) when reusing Native and Banner Ad objects
4+
15
## 0.11.0+2
26

37
* Set min Android version to `19`.

packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdInstanceManager.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ void trackAd(@NonNull FlutterAd ad, int adId) {
6969
}
7070

7171
void disposeAd(int adId) {
72+
if (!ads.containsKey(adId)) {
73+
return;
74+
}
75+
Object adObject = ads.get(adId);
76+
if (adObject instanceof FlutterDestroyableAd) {
77+
((FlutterDestroyableAd) adObject).destroy();
78+
}
7279
ads.remove(adId);
7380
}
7481

packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
/** Constants used in the plugin. */
1818
public class Constants {
1919
/** Version request agent. Should be bumped alongside plugin versions. */
20-
public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-0.11.0+2";
20+
public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-0.11.0+3";
2121
}

packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterBannerAd.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import com.google.android.gms.ads.AdView;
2121
import io.flutter.plugin.platform.PlatformView;
2222

23-
class FlutterBannerAd extends FlutterAd implements PlatformView {
23+
class FlutterBannerAd extends FlutterAd implements PlatformView, FlutterDestroyableAd {
2424
@NonNull private final AdInstanceManager manager;
2525
@NonNull private final String adUnitId;
2626
@NonNull private final FlutterAdSize size;
@@ -97,6 +97,13 @@ public View getView() {
9797

9898
@Override
9999
public void dispose() {
100+
// Do nothing. Cleanup is handled in destroy() below, which is triggered from dispose() being
101+
// called on the flutter ad object. This is allows for reuse of the ad view, for example
102+
// in a scrolling list view.
103+
}
104+
105+
@Override
106+
public void destroy() {
100107
if (view != null) {
101108
view.destroy();
102109
view = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.flutter.plugins.googlemobileads;
2+
3+
/** Interface for a destroyable ad. */
4+
public interface FlutterDestroyableAd {
5+
6+
/** Destroy any ads and free up references. */
7+
void destroy();
8+
}

packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory;
2828
import java.util.Map;
2929

30-
class FlutterNativeAd extends FlutterAd implements PlatformView {
30+
class FlutterNativeAd extends FlutterAd implements PlatformView, FlutterDestroyableAd {
3131
private static final String TAG = "FlutterNativeAd";
3232

3333
@NonNull private final AdInstanceManager manager;
@@ -141,6 +141,13 @@ public View getView() {
141141

142142
@Override
143143
public void dispose() {
144+
// Do nothing. Cleanup is handled in destroy() below, which is triggered from dispose() being
145+
// called on the flutter ad object. This is allows for reuse of the ad view, for example
146+
// in a scrolling list view.
147+
}
148+
149+
@Override
150+
public void destroy() {
144151
if (ad != null) {
145152
ad.destroy();
146153
ad = null;

packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterPublisherBannerAd.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* Wrapper around {@link com.google.android.gms.ads.doubleclick.PublisherAdView} for the Google
2828
* Mobile Ads Plugin.
2929
*/
30-
class FlutterPublisherBannerAd extends FlutterAd implements PlatformView {
30+
class FlutterPublisherBannerAd extends FlutterAd implements PlatformView, FlutterDestroyableAd {
3131
@NonNull private final AdInstanceManager manager;
3232
@NonNull private final String adUnitId;
3333
@NonNull private final List<FlutterAdSize> sizes;
@@ -126,10 +126,16 @@ public View getView() {
126126

127127
@Override
128128
public void dispose() {
129+
// Do nothing. Cleanup is handled in destroy() below, which is triggered from dispose() being
130+
// called on the flutter ad object. This is allows for reuse of the ad view, for example
131+
// in a scrolling list view.
132+
}
133+
134+
@Override
135+
public void destroy() {
129136
if (view != null) {
130137
view.destroy();
131138
view = null;
132139
}
133-
// Do nothing.
134140
}
135141
}

packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.junit.Test;
4747
import org.mockito.ArgumentCaptor;
4848
import org.mockito.ArgumentMatchers;
49+
import org.mockito.Mockito;
4950

5051
public class GoogleMobileAdsTest {
5152
private AdInstanceManager testManager;
@@ -229,22 +230,41 @@ public void showRewardedAd() {
229230
}
230231

231232
@Test
232-
public void disposeAd() {
233-
final FlutterBannerAd bannerAd =
234-
new FlutterBannerAd.Builder()
235-
.setManager(testManager)
236-
.setAdUnitId("testId")
237-
.setSize(new FlutterAdSize(1, 2))
238-
.setRequest(request)
239-
.build();
233+
public void disposeAd_banner() {
234+
FlutterBannerAd bannerAd = Mockito.mock(FlutterBannerAd.class);
240235
testManager.trackAd(bannerAd, 2);
241236
assertNotNull(testManager.adForId(2));
242237
assertNotNull(testManager.adIdFor(bannerAd));
243238
testManager.disposeAd(2);
239+
verify(bannerAd).destroy();
244240
assertNull(testManager.adForId(2));
245241
assertNull(testManager.adIdFor(bannerAd));
246242
}
247243

244+
@Test
245+
public void disposeAd_publisherBanner() {
246+
FlutterPublisherBannerAd publisherBannerAd = Mockito.mock(FlutterPublisherBannerAd.class);
247+
testManager.trackAd(publisherBannerAd, 2);
248+
assertNotNull(testManager.adForId(2));
249+
assertNotNull(testManager.adIdFor(publisherBannerAd));
250+
testManager.disposeAd(2);
251+
verify(publisherBannerAd).destroy();
252+
assertNull(testManager.adForId(2));
253+
assertNull(testManager.adIdFor(publisherBannerAd));
254+
}
255+
256+
@Test
257+
public void disposeAd_native() {
258+
FlutterNativeAd flutterNativeAd = Mockito.mock(FlutterNativeAd.class);
259+
testManager.trackAd(flutterNativeAd, 2);
260+
assertNotNull(testManager.adForId(2));
261+
assertNotNull(testManager.adIdFor(flutterNativeAd));
262+
testManager.disposeAd(2);
263+
verify(flutterNativeAd).destroy();
264+
assertNull(testManager.adForId(2));
265+
assertNull(testManager.adIdFor(flutterNativeAd));
266+
}
267+
248268
@Test
249269
public void adMessageCodec_encodeFlutterAdSize() {
250270
final AdMessageCodec codec = new AdMessageCodec();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// Constants used in the example app.
2+
class Constants {
3+
/// Placeholder text.
4+
static const String placeholderText =
5+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod'
6+
' tempor incididunt ut labore et dolore magna aliqua. Faucibus purus in'
7+
' massa tempor. Quis enim lobortis scelerisque fermentum dui faucibus'
8+
' in. Nibh praesent tristique magna sit amet purus gravida quis.'
9+
' Magna sit amet purus gravida quis blandit turpis cursus in. Sed'
10+
' adipiscing diam donec adipiscing tristique. Urna porttitor rhoncus'
11+
' dolor purus non enim praesent. Pellentesque habitant morbi tristique'
12+
' senectus et netus. Risus ultricies tristique nulla aliquet enim tortor'
13+
' at.';
14+
}

packages/google_mobile_ads/example/lib/main.dart

Lines changed: 78 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import 'dart:async';
1919
import 'package:flutter/material.dart';
2020
import 'package:google_mobile_ads/google_mobile_ads.dart';
2121

22+
import 'constants.dart';
23+
import 'reusable_inline_example.dart';
24+
2225
// You can also test with your own ad unit IDs by registering your device as a
2326
// test device. Check the logs for your device's ID value.
2427
const String testDevice = 'YOUR_DEVICE_ID';
@@ -36,28 +39,6 @@ class _MyAppState extends State<MyApp> {
3639
nonPersonalizedAds: true,
3740
);
3841

39-
final List<String> _article = <String>[
40-
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod'
41-
' tempor incididunt ut labore et dolore magna aliqua. Faucibus purus in'
42-
' massa tempor. Quis enim lobortis scelerisque fermentum dui faucibus'
43-
' in. Nibh praesent tristique magna sit amet purus gravida quis.'
44-
' Magna sit amet purus gravida quis blandit turpis cursus in. Sed'
45-
' adipiscing diam donec adipiscing tristique. Urna porttitor rhoncus'
46-
' dolor purus non enim praesent. Pellentesque habitant morbi tristique'
47-
' senectus et netus. Risus ultricies tristique nulla aliquet enim tortor'
48-
' at.',
49-
'Eget dolor morbi non arcu. Nec sagittis aliquam malesuada bibendum. Nec'
50-
' feugiat nisl pretium fusce id velit ut. Volutpat commodo sed egestas'
51-
' egestas. Ultrices neque ornare aenean euismod elementum. Consequat'
52-
' semper viverra nam libero justo laoreet sit amet cursus. Fusce ut'
53-
' placerat orci nulla pellentesque dignissim. Justo donec enim diam'
54-
' vulputate ut pharetra sit amet.',
55-
'Et malesuada fames ac turpis egestas maecenas. Augue ut lectus arcu'
56-
' bibendum at varius. Mauris rhoncus aenean vel elit scelerisque mauris'
57-
' pellentesque pulvinar. Faucibus a pellentesque sit amet porttitor'
58-
' eget dolor.'
59-
];
60-
6142
InterstitialAd _interstitialAd;
6243
bool _interstitialReady = false;
6344

@@ -148,69 +129,84 @@ class _MyAppState extends State<MyApp> {
148129
@override
149130
Widget build(BuildContext context) {
150131
return MaterialApp(
151-
home: Scaffold(
152-
appBar: AppBar(
153-
title: const Text('AdMob Plugin example app'),
154-
actions: <Widget>[
155-
PopupMenuButton<String>(
156-
onSelected: (String result) {
157-
assert(result == 'InterstitialAd' || result == 'RewardedAd');
158-
switch (result) {
159-
case 'InterstitialAd':
160-
if (!_interstitialReady) return;
161-
_interstitialAd.show();
162-
_interstitialReady = false;
163-
_interstitialAd = null;
164-
break;
165-
case 'RewardedAd':
166-
if (!_rewardedReady) return;
167-
_rewardedAd.show();
168-
_rewardedReady = false;
169-
_rewardedAd = null;
132+
home: Builder(
133+
builder: (context) => Scaffold(
134+
appBar: AppBar(
135+
title: const Text('AdMob Plugin example app'),
136+
actions: <Widget>[
137+
PopupMenuButton<String>(
138+
onSelected: (String result) {
139+
switch (result) {
140+
case 'InterstitialAd':
141+
if (!_interstitialReady) return;
142+
_interstitialAd.show();
143+
_interstitialReady = false;
144+
_interstitialAd = null;
145+
break;
146+
case 'RewardedAd':
147+
if (!_rewardedReady) return;
148+
_rewardedAd.show();
149+
_rewardedReady = false;
150+
_rewardedAd = null;
151+
break;
152+
case 'ReusableInlineExample':
153+
Navigator.push(
154+
context,
155+
MaterialPageRoute(
156+
builder: (context) => ReusableInlineExample()),
157+
);
158+
break;
159+
default:
160+
throw AssertionError('unexpected button: ${result}');
161+
}
162+
},
163+
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
164+
PopupMenuItem<String>(
165+
child: Text('$InterstitialAd'),
166+
value: '$InterstitialAd',
167+
),
168+
PopupMenuItem<String>(
169+
child: Text('$RewardedAd'),
170+
value: '$RewardedAd',
171+
),
172+
PopupMenuItem<String>(
173+
child: Text('Reusable Inline Ads Object Example'),
174+
value: 'ReusableInlineExample',
175+
),
176+
],
177+
),
178+
],
179+
),
180+
body: Padding(
181+
padding: const EdgeInsets.symmetric(horizontal: 16.0),
182+
child: ListView.separated(
183+
cacheExtent: 500,
184+
itemCount: 100,
185+
separatorBuilder: (BuildContext context, int index) {
186+
return Container(
187+
height: 40,
188+
);
189+
},
190+
itemBuilder: (BuildContext context, int index) {
191+
if (index % 2 == 0) {
192+
return Text(
193+
Constants.placeholderText,
194+
style: TextStyle(fontSize: 24),
195+
);
170196
}
197+
198+
final int adIndex = (index / 2).floor();
199+
Widget adWidget;
200+
if (adIndex % 3 == 0) {
201+
adWidget = BannerAdWidget(AdSize.banner);
202+
} else if (adIndex % 3 == 1) {
203+
adWidget = PublisherBannerAdWidget(AdSize.largeBanner);
204+
} else {
205+
adWidget = NativeAdWidget();
206+
}
207+
return Center(child: adWidget);
171208
},
172-
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
173-
PopupMenuItem<String>(
174-
child: Text('$InterstitialAd'),
175-
value: '$InterstitialAd',
176-
),
177-
PopupMenuItem<String>(
178-
child: Text('$RewardedAd'),
179-
value: '$RewardedAd',
180-
),
181-
],
182209
),
183-
],
184-
),
185-
body: Padding(
186-
padding: const EdgeInsets.symmetric(horizontal: 16.0),
187-
child: ListView.separated(
188-
cacheExtent: 500,
189-
itemCount: 100,
190-
separatorBuilder: (BuildContext context, int index) {
191-
return Container(
192-
height: 40,
193-
);
194-
},
195-
itemBuilder: (BuildContext context, int index) {
196-
if (index % 2 == 0) {
197-
return Text(
198-
_article[(index / 2).ceil() % 3],
199-
style: TextStyle(fontSize: 24),
200-
);
201-
}
202-
203-
final int adIndex = (index / 2).floor();
204-
Widget adWidget;
205-
if (adIndex % 3 == 0) {
206-
adWidget = BannerAdWidget(AdSize.banner);
207-
} else if (adIndex % 3 == 1) {
208-
adWidget = PublisherBannerAdWidget(AdSize.largeBanner);
209-
} else {
210-
adWidget = NativeAdWidget();
211-
}
212-
return Center(child: adWidget);
213-
},
214210
),
215211
),
216212
),

0 commit comments

Comments
 (0)