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

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
- Blog post
Flutter In-App Purchase Tutorial
Learn how to configure monthly and yearly subscriptions
- Blog post
RevenueCat Flutter SDK adds web support (beta)
Flutter developers can now use RevenueCat to manage subscriptions across web, mobile, and desktop platforms.