← Back to posts Cover image for Mastering In-App Subscriptions in Flutter: RevenueCat vs. `in_app_purchase` for Scalability

Mastering In-App Subscriptions in Flutter: RevenueCat vs. `in_app_purchase` for Scalability

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

So you’ve built a great Flutter app, and now it’s time to unlock recurring revenue with subscriptions. This is where many developers hit a crossroads: do you build the billing infrastructure yourself using the official in_app_purchase package, or do you leverage a third-party service like RevenueCat?

The choice isn’t trivial. It’s a classic trade-off between control and convenience, with long-term implications for your app’s scalability and your own sanity. Let’s break down both paths with practical code and clear considerations to help you decide.

The Native Route: in_app_purchase Package

The Flutter team provides the in_app_purchase package as a cross-platform wrapper around the native StoreKit (iOS) and Google Play Billing Library (Android). It gives you direct, unfiltered access to the platform APIs.

The Implementation Reality

While powerful, it’s a lower-level tool. You are responsible for handling the entire purchase flow, validating receipts, managing customer entitlements (are they subscribed?), and storing purchase history. Here’s a simplified snippet showing just the beginning of a purchase flow:

import 'package:in_app_purchase/in_app_purchase.dart';

class NativeIAPService {
  final InAppPurchase _iap = InAppPurchase.instance;

  Future<void> purchaseSubscription(String productId) async {
    // 1. Fetch product details
    final ProductDetailsResponse response = await _iap.queryProductDetails({productId});
    if (response.notFoundIDs.isNotEmpty) {
      print('Product $productId not found');
      return;
    }

    final ProductDetails product = response.productDetails.first;

    // 2. Initiate the purchase
    final PurchaseParam purchaseParam = PurchaseParam(
      productDetails: product,
      applicationUserName: null, // Optional user identifier
    );

    // 3. Listen to the purchase stream for updates
    final StreamSubscription<List<PurchaseDetails>> subscription =
        _iap.purchaseStream.listen((purchaseDetailsList) {
      _handlePurchases(purchaseDetailsList);
    });

    // 4. Launch the platform-native purchase dialog
    await _iap.buyNonConsumable(purchaseParam: purchaseParam);

    // Remember to cancel the stream subscription appropriately
    // (e.g., in a StatefulWidget's dispose method).
  }

  void _handlePurchases(List<PurchaseDetails> purchases) {
    for (final purchase in purchases) {
      switch (purchase.status) {
        case PurchaseStatus.pending:
          break;
        case PurchaseStatus.purchased:
        case PurchaseStatus.restored:
          // CRITICAL: Deliver the entitlement AND verify the receipt
          // on your own backend server for security.
          _deliverPremiumAccess(purchase.purchaseID!);
          break;
        case PurchaseStatus.error:
          _handleError(purchase.error!);
          break;
        case PurchaseStatus.canceled:
          break;
      }
      if (purchase.pendingCompletePurchase) {
        // You must call this to finish the transaction.
        InAppPurchase.instance.completePurchase(purchase);
      }
    }
  }

  void _deliverPremiumAccess(String purchaseId) {
    // Your logic to grant the user premium features.
    // This must be paired with server-side receipt validation.
  }
}

The Scalability Challenge The code above is just the tip of the iceberg. As you scale, you’ll need to build:

  • A secure backend to validate receipts from Apple/Google.
  • A database to track user subscription statuses, renewals, and expirations.
  • Webhooks to listen for real-time subscription events (e.g., cancellations, billing issues).
  • Analytics dashboards to track MRR, churn, and other key metrics.

This is the “complexity” developers talk about. It’s not that the package is buggy; it’s that it’s a foundational tool, not a complete solution.

The Managed Service Route: RevenueCat

RevenueCat sits on top of the native stores, providing a unified API and handling the complex backend infrastructure for you. You manage products in their dashboard, and they handle receipt validation, status tracking, and customer data.

Implementation in Practice

The integration shifts from managing transactions to querying a user’s entitlement status.

import 'package:purchases_flutter/purchases_flutter.dart';

class RevenueCatService {
  Future<void> initialize() async {
    await Purchases.setLogLevel(LogLevel.debug);
    await Purchases.configure(
      PurchasesConfiguration('your_public_revenuecat_sdk_key')
        ..appUserID = null // Let RevenueCat auto-generate an anonymous ID
    );
  }

  Future<void> purchasePackage(String packageId) async {
    try {
      // 1. Fetch offerings (the way RevenueCat organizes products)
      final Offerings offerings = await Purchases.getOfferings();
      final Package? package = offerings.current?.getPackage(packageId);

      if (package == null) return;

      // 2. Make the purchase. RevenueCat handles the native flow.
      final CustomerInfo customerInfo = await Purchases.purchasePackage(package);

      // 3. Check the entitlement immediately
      await _checkEntitlement(customerInfo);
    } on PlatformException catch (e) {
      // Handle errors, including user cancellation.
      print(e.message);
    }
  }

  Future<void> _checkEntitlement(CustomerInfo customerInfo) async {
    // 'premium' is the identifier you set in the RevenueCat dashboard
    final entitlement = customerInfo.entitlements.active['premium'];

    if (entitlement != null) {
      // User is actively subscribed. Grant access.
      _deliverPremiumAccess();
    } else {
      // User is not subscribed.
      _revokePremiumAccess();
    }
  }

  Stream<CustomerInfo> get customerInfoStream {
    // A powerful stream that updates in real-time as subscription status changes.
    return Purchases.addCustomerInfoUpdateListener();
  }
}

The Scalability Advantage With RevenueCat, scaling your billing is largely their problem. You get:

  • Real-time Status: The customerInfoStream notifies your app instantly if a subscription renews, expires, or is canceled.
  • Out-of-the-box Analytics: Charts for revenue, active subscribers, and churn are built-in.
  • Compliance & Trials: They help manage features like introductory pricing and free trials correctly across platforms.
  • Server Events: You can forward webhooks to your backend to trigger events (e.g., cancel a user’s cloud service when they unsubscribe).

The Verdict: Which Should You Choose?

Choose in_app_purchase if:

  • You have very specific, complex billing logic that a service can’t model.
  • You have the backend resources and expertise to build and maintain a secure subscription infrastructure.
  • You are extremely cost-sensitive and prefer the fixed cost of your own engineering time.

Choose RevenueCat if:

  • You are an indie developer or small team where developer time is your most precious resource.
  • You want to launch robust, compliant subscriptions quickly and focus on your app’s core features.
  • You need advanced subscription metrics without building internal tooling.
  • Your app’s long-term success depends on iterating quickly on pricing and monetization strategies.

Common Mistake to Avoid

Regardless of your choice, never trust the client alone. Even with RevenueCat, for mission-critical entitlements (like access to a paid API), your backend should validate a user’s status. With in_app_purchase, this is non-negotiable—you must validate receipts on your server. With RevenueCat, you can use their webhooks or server-side API to get the truth.

Final Thought

For most Flutter developers, especially those working solo or on small teams, the scalability equation leans heavily towards RevenueCat. The time you save on implementation, monitoring, and compliance is time you can spend improving your app and acquiring customers. The in_app_purchase package is an excellent and reliable tool, but it’s just that—a tool. RevenueCat provides the complete workshop.

Start by prototyping your purchase flow with RevenueCat’s generous free tier. The speed of integration will likely make the decision for you.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter State Management: Choosing the Right Solution for Your Project

Flutter State Management: Choosing the Right Solution for Your Project

Flutter developers often struggle with selecting the optimal state management solution given the myriad of options. This post will provide a practical framework for evaluating popular choices like Riverpod, Bloc, Provider, and GetX, discussing their strengths, weaknesses, and ideal use cases to help teams make informed decisions based on project complexity and team experience.

Cover image for Mastering Advanced Scrolling UI with Flutter Slivers: Beyond Basic Lists

Mastering Advanced Scrolling UI with Flutter Slivers: Beyond Basic Lists

Flutter's Sliver widgets offer powerful capabilities for creating highly customized and performant scrollable UIs, but they are often underutilized or misunderstood. This post will demystify Slivers, providing practical examples for implementing complex scroll effects like sticky headers, expanding app bars, and sections that shrink or collapse, guiding developers to build truly dynamic user interfaces.

Cover image for Mastering In-App Subscriptions in Flutter: RevenueCat vs. `in_app_purchase` for Scalability

Mastering In-App Subscriptions in Flutter: RevenueCat vs. `in_app_purchase` for Scalability

Flutter developers frequently struggle with implementing in-app purchases and subscriptions, often debating between the complexity of the native `in_app_purchase` package and the convenience of third-party solutions like RevenueCat. This post will provide a practical comparison, offering insights into implementation, monitoring, and compliance considerations to help developers choose the right strategy for their app's long-term billing needs.