Quickstart Adapty setup guide: Flutter

Last updated September 9, 2025 
by 
Quickstart Guide Flutter

Get from zero Adapty setup to displaying a paywall and processing purchases in under 60 minutes.

Pre-requisites

Before you begin, make sure you have:

  • A working Flutter app with iOS deployment configured
  • Flutter SDK 3.0 or later
  • iOS development environment (Xcode, CocoaPods)
  • At least one in-app purchase product created in App Store Connect
  • An Adapty account with your app added to the dashboard, including at least one product, paywall, and placement

Project files

To follow along with this tutorial, download the project files from GitHub. The starter branch contains the app without Adapty integration, while the main branch contains the finished project.

Install the Adapty SDK in your project

Add the Adapty Flutter SDK to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
    # Add the following to your dependencies list
      adapty_flutter: ^3.10.0

Run flutter pub get to install the dependencies.

iOS Configuration

Since Flutter apps require native iOS configuration for in-app purchases, run the following:

cd ios && pod install && cd ..

Initialize Adapty in your app

First, add these constants to your existing constants management system:

class AppConstants {
  static const String apiKey = "API key goes here";
  static const String accessLevelId = "premium";
  static const String placementId = "on_tap_history";
}

🔐 Find your public SDK key: Adapty Dashboard > Settings > API Keys

Next, initialize Adapty in your main.dart file:

// Add these import statements to the existing ones:
import 'package:adapty_flutter/adapty_flutter.dart';
import 'constants.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  try {
    // Initialize Adapty with API key
    await Adapty().activate(configuration: AdaptyConfiguration(
       apiKey: AppConstants.apiKey,
    ));

    // Set up profile update listener
    Adapty().didUpdateProfileStream.listen((profile) {
      profileManager.onProfileUpdate(profile);
    });
  } catch (e) {
    throw Exception('Failed to initialize Adapty: $e');
  }

  runApp(FocusJournalApp(profileManager: profileManager));
}

This code configures and activates the Adapty SDK.

Set up profile handling

Next, edit the file lib/providers/profile_manager.dart to add Adapty integration.

The primary changes are the addition of the Adapty profile property, the call to update the profile, and the updating of premium status based on the contents of the profile object.

// Add these imports to the existing ones:
import 'package:adapty_flutter/adapty_flutter.dart';
import '../constants.dart';

class ProfileManager extends ChangeNotifier {
  // Add this property to your list of properties
  AdaptyProfile? _customerProfile;
  ...

  // Add this getter
  AdaptyProfile? get customerProfile => _customerProfile;
  ...

  Future<void> refreshProfile() async {
    _setLoading(true);
    _setError(null);

    try {
      // Remove the network call delay and replace with:
      final profile = await Adapty().getProfile();
      _customerProfile = profile;
      _updatePremiumStatus();
      _setLoading(false);
    } catch (e) {
      _setError("Failed to refresh profile: $e");
      _setLoading(false);
    }
  }

  // Add this function to your profile_manager
  void _updatePremiumStatus() {
    if (_customerProfile == null) {
      _isPremium = false;
      return;
    }

    final accessLevel = _customerProfile!.accessLevels[AppConstants.accessLevelId];

    if (accessLevel == null) {
      _isPremium = false;
      return;
    }

    // Check for active, grace period, or lifetime access
    _isPremium = accessLevel.isActive || 
                 accessLevel.isInGracePeriod || 
                 accessLevel.isLifetime;

    notifyListeners();
  }

  // Add this function to your profile_manager:
  void onProfileUpdate(AdaptyProfile profile) {
    _customerProfile = profile;
    _updatePremiumStatus();
  }

  // All other functions in starter remain the same
}

Create the paywall view

Create a file in lib/views called paywall_view.dart with the following contents:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:adapty_flutter/adapty_flutter.dart';
import '../providers/profile_manager.dart';
import '../constants.dart';

class PaywallView extends StatefulWidget {
  const PaywallView({super.key});

  @override
  State<PaywallView> createState() => _PaywallViewState();
}

class _PaywallViewState extends State<PaywallView> {
  // AdaptyPaywall stored for potential future use (analytics, etc.)
  List<AdaptyPaywallProduct>? _products;
  bool _isLoading = true;
  bool _isPurchasing = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    _loadPaywall();
  }

  Future<void> _loadPaywall() async {
    try {
      setState(() {
        _isLoading = true;
        _error = null;
      });

      final paywall = await Adapty().getPaywall(
        placementId: AppConstants.placementId,
      );
      final products = await Adapty().getPaywallProducts(paywall: paywall);

      setState(() {
        _products = products;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = 'Failed to load paywall: $e';
        _isLoading = false;
      });
    }
  }

  Future<void> _makePurchase(AdaptyPaywallProduct product) async {
    setState(() {
      _isPurchasing = true;
      _error = null;
    });

    try {
      final purchaseResult = await Adapty().makePurchase(product: product);

      switch (purchaseResult) {
        case AdaptyPurchaseResultSuccess(profile: final profile):
          if (mounted) {
            context.read<ProfileManager>().onProfileUpdate(profile);

            // Show success and close paywall
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('Purchase successful! Welcome to Premium!'),
                backgroundColor: Colors.green,
                duration: Duration(seconds: 3),
              ),
            );

            Navigator.of(
              context,
            ).pop(true); // Return true to indicate successful purchase
          }
          break;
        case AdaptyPurchaseResultUserCancelled():
        case AdaptyPurchaseResultPending():
      }
    } catch (e) {
      if (mounted) {
        setState(() {
          _isPurchasing = false;
        });

        // Check if it's a user cancellation (don't show error for cancellations)
        final errorMessage = e.toString();
        if (errorMessage.contains('USER_CANCELLED')) {
          // User cancelled - just reset the state, don't show error
          return;
        }

        // For actual errors, show the error message
        setState(() {
          _error = 'Purchase failed: $e';
        });
      }
    }
  }

  Future<void> _restorePurchases() async {
    setState(() {
      _isPurchasing = true;
      _error = null;
    });

    try {
      final profile = await Adapty().restorePurchases();

      if (mounted) {
        context.read<ProfileManager>().onProfileUpdate(profile);

        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Purchases restored successfully!'),
            backgroundColor: Colors.green,
            duration: Duration(seconds: 3),
          ),
        );

        Navigator.of(context).pop(true);
      }
    } catch (e) {
      if (mounted) {
        setState(() {
          _error = 'Restore failed: $e';
          _isPurchasing = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Upgrade to Premium'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _buildPaywallContent(),
    );
  }

  Widget _buildPaywallContent() {
    if (_error != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Theme.of(context).colorScheme.error,
            ),
            const SizedBox(height: 16),
            Text(
              _error ?? 'Unknown error',
              textAlign: TextAlign.center,
              style: TextStyle(color: Theme.of(context).colorScheme.error),
            ),
            const SizedBox(height: 16),
            ElevatedButton(onPressed: _loadPaywall, child: const Text('Retry')),
          ],
        ),
      );
    }

    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Premium features list
          Card(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Column(
                children: [
                  Icon(Icons.star, size: 48, color: Colors.amber.shade600),
                  const SizedBox(height: 16),
                  const Text(
                    'Unlock Premium Features',
                    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 24),

                  _buildFeatureItem(
                    icon: Icons.history,
                    title: 'View Entry History',
                    description: 'Access all your past journal entries',
                  ),

                  _buildFeatureItem(
                    icon: Icons.cloud_sync,
                    title: 'Cloud Sync',
                    description: 'Keep your entries synced across devices',
                  ),

                  _buildFeatureItem(
                    icon: Icons.insights,
                    title: 'Insights & Analytics',
                    description: 'Track your journaling patterns',
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 24),

          // Products list
          if (_products != null && _products!.isNotEmpty) ...[
            const Text(
              'Choose Your Plan',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 16),

            ..._products!.map((product) => _buildProductCard(product)),
          ],

          const SizedBox(height: 24),

          // Restore purchases button
          TextButton(
            onPressed: _isPurchasing ? null : _restorePurchases,
            child: _isPurchasing
                ? const SizedBox(
                    width: 20,
                    height: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Text('Restore Purchases'),
          ),
        ],
      ),
    );
  }

  Widget _buildFeatureItem({
    required IconData icon,
    required String title,
    required String description,
  }) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Icon(icon, color: Theme.of(context).colorScheme.primary, size: 24),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: const TextStyle(fontWeight: FontWeight.w600),
                ),
                Text(
                  description,
                  style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildProductCard(AdaptyPaywallProduct product) {
    return Card(
      margin: const EdgeInsets.symmetric(vertical: 8),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        product.localizedTitle,
                        style: const TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      if (product.localizedDescription.isNotEmpty) ...[
                        const SizedBox(height: 4),
                        Text(
                          product.localizedDescription,
                          style: TextStyle(color: Colors.grey.shade600),
                        ),
                      ],
                    ],
                  ),
                ),
                Text(
                  product.price.localizedString ?? 'Price unavailable',
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.green,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _isPurchasing ? null : () => _makePurchase(product),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: _isPurchasing
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                      ),
                    )
                  : const Text('Subscribe', style: TextStyle(fontSize: 16)),
            ),
          ],
        ),
      ),
    );
  }
}

The above sets up a basic paywall view with subscription options and pricing. It’s designed to showcase the basics of interfacing with the Adapty SDK to get information on products and present them to the user. You also have the option of using our Paywall Builder feature to design and deploy paywalls from the Adapty dashboard without requiring a new submission to the App or Play stores.

IMG 2930

Update the main journal interface to show a paywall

Edit the home_view.dart file to call the paywall instead of simulating a purchase:

class _HomeViewState extends State<HomeView> {
  ...

  void _viewHistory() {
    final profileManager = context.read<ProfileManager>();

    if (profileManager.isPremium) {
      Navigator.of(context).pushNamed('/history');
    } else {
      // Remove the call to show the mock dialog and replace with:
      _showPaywall();
    }
  }

  // Add this function
  void _showPaywall() async {
    if (!mounted) return;

    final result = await Navigator.of(context).pushNamed('/paywall');

    // If purchase was successful, refresh profile and navigate to history
    if (result == true && mounted) {
      final profileManager = context.read<ProfileManager>();
      await profileManager.refreshProfile();

      if (profileManager.isPremium && mounted) {
        Navigator.of(context).pushNamed('/history');
      }
    }
  }

  ...
}

class FocusJournalApp extends StatelessWidget {
  ...

  Widget build(BuildContext context) {
    return ChangeNotifierProvider<ProfileManager>.value(
      ...
      child: MaterialApp(
        ...
        routes: {
          ...
          // Add this route to the Paywall View under the route for the History View
          '/paywall': (context) => const PaywallView(),
        },
  }
}

Check for purchases on app launch

The initialization code we added to main.dart handles checking subscription status when the app launches. The ProfileManager automatically updates the premium status based on the user’s access levels.

Here’s how the purchase flow works:

  1. App Launch: refreshProfile() gets the latest subscription status
  2. Profile Updates: The SDK automatically notifies your app when purchases are made
  3. Access Control: The isPremium flag controls which features are available

Test your integration

Build and run your app:

flutter build ios --no-codesign

The expected flow is as follows:

  • Load subscription status from Adapty on launch
  • Show premium features to subscribers
  • Display paywalls to non-subscribers
  • Process purchases and unlock content immediately

Cross-Platform Considerations

While this guide focuses on iOS, the Flutter implementation works on Android too. For Android deployment, you’ll need:

  • Google Play Console setup for in-app purchases
  • Android-specific product configuration in Adapty
  • Additional gradle configuration for billing permissions

The core Dart code remains identical across platforms.

Congrats!

At this point, your Flutter app:

  • Loads paywalls from Adapty using your remote configuration
  • Lets users purchase subscriptions through native iOS interfaces
  • Unlocks premium features based on access levels
  • Handles subscription status automatically across app launches
  • Uses Flutter best practices with Provider state management

Learn more about Flutter best practices with Adapty in our documentation.

Ben Gohlke
Developer Advocate
General

On this page

Ready to create your first paywall with Adapty?
Build money-making paywalls without coding
Get started for free