Flutter SDKでFlow & Paywall Builderを使ったアプリ内課金の有効化
アプリ内課金を有効にするには、3つの重要な概念を理解する必要があります。
- プロダクト – ユーザーが購入できるもの(サブスクリプション、消耗型アイテム、永続アクセス)
- フロー & ペイウォール – プロダクトをユーザーに提示するための画面で、ノーコードの Flow & Paywall Builder で作成します。SDK は
getFlowを通じてこれらを取得します。独自のコードで UI を構築する場合は、カスタムペイウォールの実装を参照してください。 - プレースメント – アプリ内でフロー & ペイウォールを表示する場所とタイミング(
main、onboarding、settingsなど)。ダッシュボードでプレースメントに紐付け、コード内ではプレースメント ID で取得します。これにより、A/B テストの実施や、ユーザーごとに異なるペイウォールを表示することが簡単になります。
Adaptyでは、アプリ内課金を有効にする方法を3つ提供しています。アプリの要件に応じていずれかを選択してください:
| 実装方法 | 複雑さ | 使用するタイミング |
|---|---|---|
| Adapty Flow & Paywall Builder | ✅ 簡単 | ノーコードビルダーで購入対応済みのフローまたはペイウォールを作成します。Adapty が自動でレンダリングし、複雑な購入ロジック、レシート検証、サブスクリプション管理をすべて裏側で処理します。 |
| カスタムペイウォール | 🟡 中程度 | アプリのコードでペイウォール UI を実装しますが、プロダクト提供の柔軟性を維持するために Adapty からフローオブジェクトを取得します。ガイドを参照してください。 |
| オブザーバーモード | 🔴 難しい | すでに独自の購入処理インフラがあり、それを引き続き使用したい場合に利用します。オブザーバーモードには Adapty における制限があることに注意してください。記事を参照してください。 |
以下の手順は、Adapty フロー&ペイウォールビルダーで作成したフローまたはペイウォールの実装方法を説明しています。
ペイウォールのUIを自分で構築する場合は、カスタムペイウォールの実装を参照してください。
Adapty フロー&ペイウォールビルダーで作成したフローまたはペイウォールをアプリで表示するには、アプリのコードで次のことだけ行えば大丈夫です。
- フローまたはペイウォールを取得する: Adaptyからフローまたはペイウォールを取得します。
- 表示するだけで、購入処理はAdaptyが担当: アプリ内でビューを表示します。
- ボタン操作を処理する: ユーザーの操作に対するアプリの応答を関連付けます。たとえば、ユーザーがボタンをクリックした際にリンクを開いたり、ペイウォールを閉じたりします。
始める前に
始める前に、以下の手順を完了してください:
- Adapty ダッシュボードで App Store および/または Google Play にアプリを接続する。
- Adapty でプロダクトを作成する。
- フローまたはペイウォールを作成し、プロダクトを追加する。
- プレースメントを作成し、フローまたはペイウォールを追加する。
- アプリのコードに Adapty SDK をインストールして有効化する。このガイドでは Adapty Flutter SDK v4 の API を使用します。
これらのステップを最も素早く完了するには、クイックスタートガイドに従うか、Developer CLIを使用してペイウォールとプレースメントを作成してください。
1. フローまたはペイウォールを取得する
フローとペイウォールは、ダッシュボードで設定されたプレースメントに関連付けられています。プレースメントを使用すると、異なるオーディエンスに対して異なるフローやペイウォールを表示したり、A/B テストを実行したりできます。
Adapty のフロー & ペイウォールビルダーで作成したフローまたはペイウォールを取得するには、以下の手順に従います。
-
getFlowメソッドを使用してプレースメント ID からflowオブジェクトを取得し、hasViewConfigurationプロパティを使ってそのオブジェクトがビルダーで作成されたかどうかを確認します。 -
createFlowViewメソッドを使用してフロービューを作成します。ビューには、表示に必要なUI要素とスタイルが含まれています。
ビューの設定を取得するには、フローまたはペイウォールを公開してください。未公開の編集があるフローまたはペイウォールは Dirty ステータスになり、そのプレースメントは最後に公開されたバージョンを引き続き提供します。
try {
// the requested flow
final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID');
final view = await AdaptyUI().createFlowView(
flow: flow,
);
} on AdaptyError catch (adaptyError) {
// handle the error
} catch (e) {
// handle the error
}
2. フローまたはペイウォールを表示する
フロービューを取得したら、あとは数行追加するだけで表示できます。
フローを表示するには、createFlowView メソッドで作成した view に対して view.present() メソッドを呼び出します。各 view は一度しか表示できません。dismissすると、ビューはメモリから解放されます。再度フローを表示する必要がある場合は、createFlowView をもう一度呼び出して新しい view インスタンスを作成してください。
try {
await view.present();
} on AdaptyError catch (e) {
// handle the error
} catch (e) {
// handle the error
}
フローやペイウォールの表示方法の詳細については、ガイドをご覧ください。
3. ボタンアクションの処理
フローやペイウォールでユーザーがボタンをクリックすると、Flutter SDK は購入、復元、画面のクローズ、URL のオープンを自動的に処理します。ただし、カスタムまたは事前定義された ID を持つボタンは、コード内でアクションを処理する必要があります。
フロー画面のプロセスを制御・監視するには、AdaptyUIFlowsEventsObserver のメソッドを実装し、画面を表示する前にオブザーバーを設定してください。ユーザーが何らかのアクションを実行すると flowViewDidPerformAction が呼び出されるため、アクション ID に応じてアプリが適切に応答する必要があります。
3つのオブザーバーメソッドが必須です:flowViewDidFinishPurchase、flowViewDidFinishRestore、flowViewDidReceiveError — これらがないとクラスはコンパイルエラーになります。
ウィジェットではなく、専用の長命なオブジェクトとしてオブザーバーを実装してください。アプリ全体で1つのグローバルなオブザーバースロットが共有されるため、State にバインドするとその画面がリークし(SDKが強参照を保持するため)、次の画面が登録された時点でサイレントに上書きされてしまいます。extends を使うことでSDKのデフォルト動作も継承されるため、必須の3つのメソッドに加え、必要なコールバックだけをオーバーライドすれば十分です。
// A dedicated, long-lived handler for flow events.
// It does NOT live inside a Widget/State, so it never leaks and is never
// silently replaced when screens are pushed or popped.
class FlowEventsHandler extends AdaptyUIFlowsEventsObserver {
// A single, app-wide instance — same idiom as Adapty() and AdaptyUI().
static final FlowEventsHandler _instance = FlowEventsHandler._();
factory FlowEventsHandler() => _instance;
FlowEventsHandler._();
// This method is called when user performs an action on the flow UI.
// Overriding it replaces the default behavior (dismiss on close, open URLs),
// so keep those cases if you want to preserve it.
@override
void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) {
switch (action) {
case const CloseAction():
case const AndroidSystemBackAction(): // close the flow on the Android back button
view.dismiss();
break;
case OpenUrlAction(:final url, :final openIn):
AdaptyUI().openUrl(url, openIn: openIn);
break;
default:
break;
}
}
// Required: decide what happens after a purchase finishes
@override
void flowViewDidFinishPurchase(AdaptyUIFlowView view,
AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) {
if (purchaseResult is! AdaptyPurchaseResultUserCancelled) {
view.dismiss();
}
}
// Required: dismiss the flow once a restore succeeds
@override
void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) {
view.dismiss();
}
// Required: handle rendering and other view errors
@override
void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) {
print('Flow error: $error');
view.dismiss();
}
}
ハンドラーは、フローやペイウォールが表示される前に、アプリ起動時に一度だけ登録してください:
AdaptyUI().setFlowsEventsObserver(FlowEventsHandler());
次のステップ
ご質問やお困りのことがあれば、サポートフォーラムをご覧ください。よくある質問への回答を見つけたり、ご自身の質問を投稿することができます。チームとコミュニティがサポートいたします!
フローまたはペイウォールをアプリ内で表示する準備が整いました。App Store サンドボックスまたはGoogle Play Storeでテスト購入を行い、フローからテスト購入を完了できることを確認してください。
次に、ユーザーのアクセスレベルを確認して、適切なユーザーにペイウォールを表示したり、有料機能へのアクセスを付与したりできるようにする必要があります。
完全なコード例
以下に、これらのすべてのステップをアプリに統合した場合の完全なサンプルコードを示します。
void main() {
// Register a single, long-lived observer once, before any flow is shown.
// It is intentionally a plain object (NOT a Widget/State): its lifetime is the
// whole app, so it never leaks and is never silently replaced when screens are
// pushed or popped.
AdaptyUI().setFlowsEventsObserver(FlowEventsHandler());
runApp(MaterialApp(home: FlowScreen()));
}
/// A dedicated handler for AdaptyUI flow events.
///
/// It `extends` [AdaptyUIFlowsEventsObserver] (rather than being implemented
/// by a `State`), which gives you two things for free:
/// * the SDK's sensible defaults for optional callbacks, so besides the three
/// required methods you only override what you actually care about;
/// * a lifecycle that is independent of the widget tree — there is no strong
/// reference back into a `Widget`, so nothing leaks and there is nothing to
/// unregister.
///
/// Every callback receives the [AdaptyUIFlowView] it relates to, so handling
/// flow actions never requires a `BuildContext` or widget state.
class FlowEventsHandler extends AdaptyUIFlowsEventsObserver {
// A single, app-wide instance — same idiom as Adapty() and AdaptyUI().
static final FlowEventsHandler _instance = FlowEventsHandler._();
factory FlowEventsHandler() => _instance;
FlowEventsHandler._();
// Called when the user performs an action on the flow UI.
@override
void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) {
switch (action) {
case const CloseAction():
case const AndroidSystemBackAction(): // close the flow on the Android back button
view.dismiss();
break;
case OpenUrlAction(:final url, :final openIn):
// Open the URL natively, honoring the dashboard browser setting.
AdaptyUI().openUrl(url, openIn: openIn);
break;
default:
break;
}
}
// Required: decide what happens after a purchase finishes.
@override
void flowViewDidFinishPurchase(AdaptyUIFlowView view,
AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) {
if (purchaseResult is! AdaptyPurchaseResultUserCancelled) {
view.dismiss();
}
}
// Required: dismiss the flow once a restore succeeds.
@override
void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) {
view.dismiss();
}
// Required: handle rendering and other view errors.
@override
void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) {
print('Flow error: $error');
view.dismiss();
}
}
class FlowScreen extends StatefulWidget {
const FlowScreen({super.key});
@override
State<FlowScreen> createState() => _FlowScreenState();
}
class _FlowScreenState extends State<FlowScreen> {
@override
void initState() {
super.initState();
_showFlowIfNeeded();
}
Future<void> _showFlowIfNeeded() async {
try {
final flow = await Adapty().getFlow(
placementId: 'YOUR_PLACEMENT_ID',
);
if (!flow.hasViewConfiguration) return;
final view = await AdaptyUI().createFlowView(flow: flow);
await view.present();
} catch (_) {
// Handle any errors (network, SDK issues, etc.)
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Adapty Flow Example')),
body: Center(
// Add a button to re-trigger the flow for testing purposes.
child: ElevatedButton(
onPressed: _showFlowIfNeeded,
child: const Text('Show Flow'),
),
),
);
}
}