← 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's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.

Cover image for Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.

Cover image for Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.