A Beginner’s guide to implementing an ad-free subscription in your Flutter app

A step-by-step tutorial to let users pay to remove ads—using AdMob, and RevenueCat

Perttu Lähteenlahti
Published

Summary

Learn how to implement a pay-to-remove-ads subscription in Flutter using AdMob and RevenueCat. This guide walks you through building an iOS and Android app that shows ads to unsubscribed users and hides them after a subscription purchase—with full code examples and setup instructions.

Mobile apps primarily rely on two monetization strategies: ads and subscriptions. While ads provide free access to users and generate revenue for developers, they often disrupt the user experience and can lead to frustration. This creates an opportunity for developers to offer a premium, ad-free experience through subscriptions—a win-win solution that enhances user satisfaction while generating recurring revenue.

In this guide, we’ll walk you through the process of implementing an ad-free subscription in your Flutter app using RevenueCat. This tutorial has you covered even if you’re new to subscriptions and ads. Things we will be doing in this tutorial:

  • Adding ads to our app using AdMob and google_mobile_ads package
  • Adding subscriptions to our app using RevenueCat and flutter_purchases
  • Configuring ads to be shown only if user is currently not subscribed, after subscribing all ads will be hidden from the users view

By the end of this article you should have a Flutter app for iOS and Android, that is capable of showing ads, displaying a paywall, and after purchasing a subscription from that paywall also able to hide the ads for the subscribed user.

Prerequisites

In this tutorial, we will build a simple Flutter app, for both iOS and Android, that allows the user to subscribe to a monthly subscription which unlocks an ad-free experience in the app. If you have an existing app in which you’re going to implement subscription functionality, you can jump ahead to the next section. 

Start by creating a new Flutter project. Follow the steps in this guide if you need a refresher. Open the project in your editor of choice. After that, run:

1$ flutter run

Testing in-app purchases is best on a real device, so choose that option from the presented list and check that your app runs without problems.

Set up App Store and Play Store

Before we can move to implementing subscriptions in our app, we need to configure our subscription product for all the platforms we want to target. Since Flutter is a cross-platform framework, it’s a good idea to target both iOS and Android. In both cases we want to configure a single subscription: a monthly recurring subscription for 5 dollars.

Create yourself a RevenueCat account and then follow this guide to connect the App Store and Play store to RevenueCat. 

Step 1: Add ads to your Flutter app

Displaying ads in the app can be done with Google AdMob. Using it requires creating an account, create one if you haven’t already. After creating your account, create the new apps in the AdMob dashboard for all the platforms you plan to target. 

Next step is to integrate the Google Mobile ADs SDK into a Flutter app. Add google_mobile_ads to the pubspec.yaml file:

1# pubspec.yaml
2dependencies:
3  google_mobile_ads: ^6.0.0
4




We need to add AdMob App IDs to both the AndroidManifest.xml file (for Android) and the Info.plist file (for iOS) Make the following changes:

1//Info.plist
2<key>GADApplicationIdentifier</key>
3<string>your-app-id-here</string>
4
1//AndroidManifest.xml
2<manifest>
3    <application>
4        <meta-data
5            android:name="com.google.android.gms.ads.APPLICATION_ID"
6            android:value="your-app-id-here"/>
7    <application>
8<manifest>
9

Before we add the code for initializing the ads (and later subscriptions), let’s go over the project structure. Both the subscription and ads logic will be placed in a file called subscription_manager.dart. Create that file inside a services folder and paste the following contents:

1import 'package:google_mobile_ads/google_mobile_ads.dart';
2import '../config/app_config.dart';
3
4class SubscriptionManager {
5  static const String _bannerAdUnitId = AppConfig.bannerAdUnitId;
6  
7  static final SubscriptionManager _instance = SubscriptionManager._internal();
8  factory SubscriptionManager() => _instance;
9  SubscriptionManager._internal();
10
11  bool _isInitialized = false;
12
13  Future<void> initialize() async {
14    if (_isInitialized) return;
15
16    try {
17      // Initialize AdMob
18      await MobileAds.instance.initialize();
19      
20      _isInitialized = true;
21    } catch (e) {
22      print('Failed to initialize services: $e');
23    }
24  }
25}

Next we need to initialize the ads at app startup. In the main.dart add this code to call the code :

1void main() async {
2  WidgetsFlutterBinding.ensureInitialized();
3  
4  // Initialize subscription manager
5  await SubscriptionManager().initialize();
6  
7  runApp(const MyApp());
8}
9
10

Displaying Banner ads

Create a new file called ad_banner_widget.dart file and paste the contents from below:

1import 'package:flutter/material.dart';
2import 'package:google_mobile_ads/google_mobile_ads.dart';
3import '../services/subscription_manager.dart';
4
5class BannerAdWidget extends StatefulWidget {
6  const BannerAdWidget({super.key});
7
8  
9  State<BannerAdWidget> createState() => _BannerAdWidgetState();
10}
11
12class _BannerAdWidgetState extends State<BannerAdWidget> {
13  BannerAd? _bannerAd;
14  bool _isLoaded = false;
15  final SubscriptionManager _subscriptionManager = SubscriptionManager();
16
17  
18  void initState() {
19    super.initState();
20    _loadAd();
21  }
22
23  void _loadAd() {
24    _bannerAd = _subscriptionManager.createBannerAd();
25    _bannerAd!.load().then((_) {
26      if (mounted) {
27        setState(() {
28          _isLoaded = true;
29        });
30      }
31    }).catchError((error) {
32      print('Failed to load banner ad: $error');
33    });
34  }
35
36  
37  void dispose() {
38    _bannerAd?.dispose();
39    super.dispose();
40  }
41
42  
43  Widget build(BuildContext context) {
44    if (!_isLoaded || _bannerAd == null) {
45      return SizedBox(
46        height: MediaQuery.of(context).size.height / 3,
47        child: const Center(
48          child: CircularProgressIndicator(),
49        ),
50      );
51    }
52
53    return Container(
54      width: double.infinity,
55      height: MediaQuery.of(context).size.height / 3,
56      decoration: BoxDecoration(
57        borderRadius: BorderRadius.circular(8),
58        border: Border.all(color: Colors.grey.shade300),
59      ),
60      child: ClipRRect(
61        borderRadius: BorderRadius.circular(8),
62        child: AdWidget(ad: _bannerAd!),
63      ),
64    );
65  }
66}
67

This component will display banner ads between the paragraphs that span the full width and one-third of the height of the page. We also need to add a functionality to the SubscriptionManager for creating banner ads:

1import 'package:google_mobile_ads/google_mobile_ads.dart';
2import '../config/app_config.dart';
3
4class SubscriptionManager {
5  static const String _bannerAdUnitId = AppConfig.bannerAdUnitId;
6  
7  static final SubscriptionManager _instance = SubscriptionManager._internal();
8  factory SubscriptionManager() => _instance;
9  SubscriptionManager._internal();
10
11  bool _isInitialized = false;
12
13  Future<void> initialize() async {
14    if (_isInitialized) return;
15
16    try {
17      // Initialize AdMob
18      await MobileAds.instance.initialize();
19      
20      _isInitialized = true;
21    } catch (e) {
22      print('Failed to initialize services: $e');
23    }
24  }
25
26  Future<bool> isSubscribed() async {
27    try {
28      CustomerInfo customerInfo = await Purchases.getCustomerInfo();
29      return customerInfo.entitlements.active.containsKey(_entitlementId);
30    } catch (e) {
31      print('Failed to check subscription status: $e');
32      return false;
33    }
34  }
35
36  BannerAd createBannerAd() {
37    return BannerAd(
38      adUnitId: _bannerAdUnitId,
39      size: AdSize.banner,
40      request: const AdRequest(),
41      listener: BannerAdListener(
42        onAdFailedToLoad: (ad, error) {
43          print('Banner ad failed to load: $error');
44          ad.dispose();
45        },
46      ),
47    );
48  }
49}
50

Update the main.dart file to make use of the new Banner ads:

1Widget build(BuildContext context) {
2    if (_loading) {
3      return const Scaffold(
4        body: Center(child: CircularProgressIndicator()),
5      );
6    }
7
8    return Scaffold(
9      appBar: AppBar(
10        backgroundColor: Theme.of(context).colorScheme.inverseSurface,
11        title: const Text('Article'),
12        actions: [
13          IconButton(
14            icon: Icon(
15              _isSubscribed ? Icons.star : Icons.star_border,
16              color: _isSubscribed ? Colors.amber : null,
17            ),
18            onPressed: _openSubscriptionScreen,
19            tooltip: _isSubscribed ? 'Ad free' : 'Go Ad free',
20          ),
21        ],
22      ),
23      body: SingleChildScrollView(
24        padding: const EdgeInsets.all(16),
25        child: Column(
26          crossAxisAlignment: CrossAxisAlignment.start,
27          children: [
28            Text(
29              'Understanding Flutter Development for Beginners',
30              style: Theme.of(context).textTheme.headlineMedium?.copyWith(
31                fontWeight: FontWeight.bold,
32              ),
33            ),
34            const SizedBox(height: 16),
35
36            _buildParagraph(
37              'One of the main benefits of Flutter is the ability to share code '
38              'between platforms. This means you can maintain a single codebase for '
39              'both iOS and Android applications, significantly reducing development '
40              'time and effort.',
41            ),
42
43            if (!_isSubscribed) ...[
44              const SizedBox(height: 12),
45              const BannerAdWidget(),
46              const SizedBox(height: 12),
47            ],
48
49            _buildParagraph(
50              'Flutter\'s widget-based architecture makes it easy to build '
51              'complex UIs with reusable components. Everything in Flutter is a '
52              'widget, from simple text elements to complex layouts.',
53            ),
54
55            if (!_isSubscribed) ...[
56              const SizedBox(height: 12),
57              const BannerAdWidget(),
58              const SizedBox(height: 12),
59            ],
60
61            _buildParagraph(
62              'With Flutter, you can also target web and desktop platforms '
63              'from the same codebase, making it a truly cross-platform solution '
64              'for modern application development.',
65            ),
66          ],
67        ),
68      ),
69    );
70  }

After these code changes, your apps should look like this:

Step 2: set up products

Once you’ve configured the store of your choice, or both, it’s time to set up Revenuecat. If you’re not familiar with Revenuecat, or time has passed since the last time you used it, this quickstart guide will help you get up to speed. 

You need to configure your RevenueCat products, offerings and entitlements to have:

  • One entitlement for the ad-free subscription level, using the identifier ad_free_entitlement
  • Two separate products for App Store and Play Store, for example with identifier ad_free_monthly (learn more about the naming practices: iOS, Android)
  • Matching subscriptions in Play Store console and App Store connect with the same identifiers

Remember to group your product under the entitlement you created.  

Step 3: Add subscriptions to your Flutter app

Now that we have everything configured, it’s time to add purchases_flutter and purchases_flutter_ui; the Flutter client for RevenueCat subscriptions and paywall UI components. Add the dependencies:

1# pubspec.yaml
2dependencies:
3  google_mobile_ads: ^6.0.0
4  purchases_flutter: ^8.10.5
5  purchases_ui_flutter: ^8.10.5
6

We want RevenueCat to be initialized when the application starts, so at the same time as we initialize the ads. Update SubscriptionManager:

1import 'package:google_mobile_ads/google_mobile_ads.dart';
2import 'package:purchases_flutter/purchases_flutter.dart';
3import '../config/app_config.dart';
4
5class SubscriptionManager {
6  static const String _apiKey = AppConfig.revenueCatApiKey;
7  static const String _entitlementId = AppConfig.entitlementId;
8  static const String _bannerAdUnitId = AppConfig.bannerAdUnitId;
9  
10  static final SubscriptionManager _instance = SubscriptionManager._internal();
11  factory SubscriptionManager() => _instance;
12  SubscriptionManager._internal();
13
14  bool _isInitialized = false;
15
16  Future<void> initialize() async {
17    if (_isInitialized) return;
18
19    try {
20      // Initialize RevenueCat
21      PurchasesConfiguration configuration = PurchasesConfiguration(_apiKey);
22      await Purchases.configure(configuration);
23      
24      // Initialize AdMob
25      await MobileAds.instance.initialize();
26      
27      _isInitialized = true;
28    } catch (e) {
29      print('Failed to initialize services: $e');
30    }
31  }
32
33  Future<bool> isSubscribed() async {
34    try {
35      CustomerInfo customerInfo = await Purchases.getCustomerInfo();
36      return customerInfo.entitlements.active.containsKey(_entitlementId);
37    } catch (e) {
38      print('Failed to check subscription status: $e');
39      return false;
40    }
41  }
42
43  BannerAd createBannerAd() {
44    return BannerAd(
45      adUnitId: _bannerAdUnitId,
46      size: AdSize.banner,
47      request: const AdRequest(),
48      listener: BannerAdListener(
49        onAdFailedToLoad: (ad, error) {
50          print('Banner ad failed to load: $error');
51          ad.dispose();
52        },
53      ),
54    );
55  }
56}
57

You can use the RevenueCat public API key directly or use an app_config.dart file like in this example. If you see RevenueCat error logs in the console, review and fix them.

Checking user entitlements

To keep track of whether a user has purchased a subscription and is eligible for ad-free experience, we implement a helper service. Add a function for checking if user is subscribed:

1// subscription_manager.dart
2
3// existing code
4
5Future<bool> isSubscribed() async {
6    try {
7      CustomerInfo customerInfo = await Purchases.getCustomerInfo();
8      return customerInfo.entitlements.active.containsKey(_entitlementId);
9    } catch (e) {
10      print('Failed to check subscription status: $e');
11      return false;
12    }
13  }
14

Using the entitlement status in your UI

Before showing ads we need to check if user is subscribed:

1import 'dart:io';
2import 'package:flutter/material.dart';
3import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';
4import 'services/subscription_manager.dart';
5import 'widgets/banner_ad_widget.dart';
6import 'config/app_config.dart';
7
8void main() async {
9  WidgetsFlutterBinding.ensureInitialized();
10  
11  
12  // Initialize subscription manager
13  await SubscriptionManager().initialize();
14  
15  runApp(const MyApp());
16}
17
18class MyApp extends StatelessWidget {
19  const MyApp({super.key});
20
21  
22  Widget build(BuildContext context) {
23    return MaterialApp(
24      title: AppConfig.appName,
25      theme: ThemeData(
26        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
27        useMaterial3: true,
28      ),
29      home: const ArticlePage(),
30    );
31  }
32}
33
34class ArticlePage extends StatefulWidget {
35  const ArticlePage({super.key});
36
37  
38  State<ArticlePage> createState() => _ArticlePageState();
39}
40
41class _ArticlePageState extends State<ArticlePage> {
42  final SubscriptionManager _subscriptionManager = SubscriptionManager();
43  bool _isSubscribed = false;
44  bool _loading = true;
45
46  
47  void initState() {
48    super.initState();
49    _checkSubscriptionStatus();
50  }
51
52  
53  Widget build(BuildContext context) {
54    if (_loading) {
55      return const Scaffold(
56        body: Center(child: CircularProgressIndicator()),
57      );
58    }
59
60    return Scaffold(
61      appBar: AppBar(
62        backgroundColor: Theme.of(context).colorScheme.inverseSurface,
63        title: const Text('Article'),
64        actions: [
65          IconButton(
66            icon: Icon(
67              _isSubscribed ? Icons.star : Icons.star_border,
68              color: _isSubscribed ? Colors.amber : null,
69            ),
70            onPressed: _openSubscriptionScreen,
71            tooltip: _isSubscribed ? 'Ad free' : 'Go Ad free',
72          ),
73        ],
74      ),
75      body: SingleChildScrollView(
76        padding: const EdgeInsets.all(16),
77        child: Column(
78          crossAxisAlignment: CrossAxisAlignment.start,
79          children: [
80            Text(
81              'Understanding Flutter Development for Beginners',
82              style: Theme.of(context).textTheme.headlineMedium?.copyWith(
83                fontWeight: FontWeight.bold,
84              ),
85            ),
86            const SizedBox(height: 16),
87
88     
89            _buildParagraph(
90              'Flutter is a popular framework for building mobile applications. It '
91              'allows developers to use Dart programming language along with native '
92              'platform capabilities to create apps for iOS and Android.',
93            ),
94
95            if (!_isSubscribed) ...[
96              const SizedBox(height: 12),
97              const BannerAdWidget(),
98              const SizedBox(height: 12),
99            ],
100
101    
102          ],
103        ),
104      ),
105    );
106  }
107

Implement purchasing functionality

Now that we can check if a user has purchased a subscription, it’s time to implement the purchasing functionality. We can do this easily with RevenueCat paywalls, which you can then customize through RevenueCat’s dashboard. Learn more about paywalls here.

Add the code below to enable showing paywalls:

1// Existing class and state declaration
2class _ArticlePageState extends State<ArticlePage> {
3  final SubscriptionManager _subscriptionManager = SubscriptionManager();
4  bool _isSubscribed = false;
5  bool _loading = true;
6
7  @override
8  void initState() {
9    super.initState();
10    _checkSubscriptionStatus();
11  }
12
13  // Check if user has active entitlement
14  Future<void> _checkSubscriptionStatus() async {
15    final isSubscribed = await _subscriptionManager.isSubscribed();
16    if (mounted) {
17      setState(() {
18        _isSubscribed = isSubscribed;
19        _loading = false;
20      });
21    }
22  }
23
24  // Show RevenueCat paywall
25  Future<void> _purchaseSubscription() async {
26    try {
27      await RevenueCatUI.presentPaywall(); // Opens default paywall configured in RevenueCat
28
29      // Refresh subscription status after paywall closes
30      await _checkSubscriptionStatus();
31
32      if (mounted && _isSubscribed) {
33        ScaffoldMessenger.of(context).showSnackBar(
34          const SnackBar(
35            content: Text('Subscription activated successfully!'),
36            backgroundColor: Colors.green,
37          ),
38        );
39      }
40    } catch (e) {
41      if (mounted) {
42        ScaffoldMessenger.of(context).showSnackBar(
43          SnackBar(
44            content: Text('Failed to show paywall: $e'),
45            backgroundColor: Colors.red,
46          ),
47        );
48      }
49    }
50  }
51@override
52  Widget build(BuildContext context) {
53    if (_loading) {
54      return const Scaffold(
55        body: Center(child: CircularProgressIndicator()),
56      );
57    }
58
59    return Scaffold(
60      appBar: AppBar(
61        backgroundColor: Theme.of(context).colorScheme.inverseSurface,
62        title: const Text('Article'),
63        actions: [
64          IconButton(
65            icon: Icon(
66              _isSubscribed ? Icons.star : Icons.star_border,
67              color: _isSubscribed ? Colors.amber : null,
68            ),
69            onPressed: _openSubscriptionScreen,
70            tooltip: _isSubscribed ? 'Ad free' : 'Go Ad free',
71          ),
72        ],
73      ),
74
75body: SingleChildScrollView(
76        padding: const EdgeInsets.all(16),
77        child: Column(
78          crossAxisAlignment: CrossAxisAlignment.start,
79          children: [
80            Text(
81              'Understanding Flutter Development for Beginners',
82              style: Theme.of(context).textTheme.headlineMedium?.copyWith(
83                fontWeight: FontWeight.bold,
84              ),
85            ),
86            const SizedBox(height: 16),
87
88            if (!_isSubscribed) ...[
89              SizedBox(
90                width: double.infinity,
91                child: ElevatedButton(
92                  onPressed: _purchaseSubscription,
93                  style: ElevatedButton.styleFrom(
94                    backgroundColor: Colors.green,
95                    foregroundColor: Colors.white,
96                    padding: const EdgeInsets.symmetric(vertical: 16),
97                    shape: RoundedRectangleBorder(
98                      borderRadius: BorderRadius.circular(8),
99                    ),
100                  ),
101                  child: const Text(
102                    'Remove Ads - Subscribe Now',
103                    style: TextStyle(
104                      fontSize: 16,
105                      fontWeight: FontWeight.bold,
106                    ),
107                  ),
108                ),
109              ),
110              const SizedBox(height: 16),
111            ],
112	//... rest of the code
113}
114

Run the app on a real device, press the button, and complete the purchase flow. After a successful purchase, the app will update to remove ads and give the user the ad-free experience.

Conclusion

Great work! You’ve successfully built a Flutter app that lets users subscribe to unlock an ad-free experience. From integrating AdMob for ads to implementing subscriptions with RevenueCat, you now have a strong foundation for monetizing your app. From here, you can take things further—customize your paywall directly in the RevenueCat dashboard, experiment with different pricing strategies, or add premium features that offer even more value behind the subscription. This is just the beginning of what’s possible with a flexible, subscription-based model.

You might also like

Share this post