【はじめてのAndroidアプリ開発】テスト用課金アプリを簡単に作ってみる



【はじめてのAndroidアプリ開発】テスト用課金アプリを簡単に作ってみる

Androidのアプリ開発をはじめようと考えている方、もしくははじめたばかりの方向けにお届けしている 【はじめてのAndroidアプリ開発】 シリーズ。 今回は 『アプリ内課金』の例をご紹介。

課金の実装っていくつかのプロセスが必要で、頭の中が整理しにくいですが、本稿は課金アプリを理解する最初のステップに最適と思います。商品登録など行わず、Googleのチュートリアルに沿って課金をイメージしていきます。

★本稿はこんな方に役立ちます★

  • Android アプリ開発初心者
  • 参考書を開いて、フリーズした方
  • 副業としてアプリ開発を考えてる方
  • アプリの収益化を考えてる方

★今回使用する環境等★

  • エミュレーター/ 実機を使用
  • プログラミング言語/ Java

参考にした資料: Google公式ドキュメント / Buy and Subscribe: Monetize your app on Google Play

目次
  1. 【はじめてのAndroidアプリ開発】テスト用課金アプリを簡単に作ってみる
  2. アプリ内課金の魅力
  3. 課金アプリ実装までの流れ
  4. 課金アプリのサンプル
  5. 課金アプリのサンプルを実行する様子
  6. プログラムの内容
  7. まとめ

【はじめてのAndroidアプリ開発】テスト用課金アプリを簡単に作ってみる

アプリ内課金の魅力

アプリの収益モデルは、主に以下の 3つが代表的。

  • 有料アプリの販売
  • アプリ内広告
  • アプリ内課金

そして各収益モデルの合計値を足した世界のアプリ売上推移は、以下のように。

img: Business of Apps

2016年 88億ドル(約1兆円)だったアプリの売上高は、2020年には 2倍強の 188億ドル(約2.2兆円) になる見込み。そしてその売上高の内訳は以下のように推移。

img: Business of Apps

広告(青色:Advertising)による売上は微増なのに比べ、 アプリ内課金(紺色:In-app purchases)は順調に右肩上がり。明らかに成功しているビジネスモデルといえるでしょう。

またアプリマーケティング研究所などの成功事例を見ていても、アプリ内課金で収益を上げている方が多い様子。

【アプリ内課金成功事例】

このようにアプリ内課金は、アプリのビジネスモデルにおいて上昇トレンドですし、成功している方も多いので、これからアプリを作ろうと考えている方にとっては知っておきたい機能の一つといえるでしょう。

課金アプリ実装までの流れ

テーマ 分類 項目 詳細
課金アプリ テスト環境 アプリの準備 デベロッパー登録不要版
デベロッパー登録必要版
デベロッパー登録
アプリの登録 本番用
テスト用
商品の登録
本番環境

アプリに課金機能を実装しようと思うと、上記のように 4つの項目のステップを踏む必要があります。 Google で「アプリ 課金 実装」などと検索すると、 デベロッパー登録やアプリ登録、商品登録の部分がよく紹介。しかしこれからアプリを作っていこうと考えている方にとっては、肝心の課金に対応したアプリがないと、イメージできないと思います。

そこで今回は上記の表赤文字で示してある「デベロッパー登録不要型の課金型アプリ」を作成していきたいと思います。

課金アプリのサンプル

image

アプリをイチから作っていこうと思うとちょっと大変。そこで今回は Google の公式ドキュメントに紹介されているサンプルを用いて、課金アプリを作っていきたいと思います。

参考ページ: Buy and Subscribe: Monetize your app on Google Play

【サンプルアプリの概要】

APP ID/ com.codelab.sample
License/ Apache 2.0
アプリの概要/ レーシングゲームで必要な商品購入の一連の流れを確認、ただしゲームプログラムは未搭載
商品の状況/ Google Play Console に登録済み
登録商品/ Gas、 UpGrade Your Cars、 月額利用パス、 年間利用パス
課金/ テスト様に作成されているサンプルアプリなので、商品の購入プロセスを踏んでも、実際に課金されることはありません。

Google Play Consol:Androidアプリを公開する時に利用する Webサービス。アプリの統計や課金などの設定も Google Play Consol で行う。

課金アプリのサンプルを実行する様子

上記動画では現れませんでしたが、場合によっては Android Studio を起動して、サンプルプロジェクトを読み込んだ時に SDK のエラーが出るかもしれません。

【SDKエラーの様子】

image

ERROR: Failed to find Build Tools revision 25.0.0
Install Build Tools 25.0.0 and sync project

これは読み込んだ内容のプロジェクトが Android のバージョン 25 であるのに対して、現在の Android Studio にはバージョン 25に対応したソフトがないために発生。エラー文の Install Build Tools 25.0.0 and sync project をクリックして SDK をインストールしましょう。

プログラムの内容

チュートリアルでは詳しくコードを紹介してくれているものの、アプリ開発初心者にとってはそのコードを一体どこに貼り付ければいいかわからないもの。またプログラムの実行結果を Logcat で確認していきますが、英単語が羅列した中から特定の文字を見つけるのはちょっと疲れます。

今回はそうした分かりにくい部分を改善して、課金アプリのイメージを掴みやすいように編集してみました。

Google Play Console との接続チェック

チュートリアル:4. Integrate with Play Billing Library

画像クリックで拡大表示

【編集ファイル: クラス BillingManager】

書き換え部分

変更前
public class BillingManager {

変更後
public class BillingManager implements PurchasesUpdatedListener {

追加

private final BillingClient mBillingClient;
private final Activity mActivity;

public BillingManager(Activity activity) {
    mActivity = activity;
    mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();
    mBillingClient.startConnection(new BillingClientStateListener() {
        @Override
        public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponse) {
            if (billingResponse == BillingClient.BillingResponse.OK) {
                Log.i(TAG, "接続確認:onBillingSetupFinished() response: " + billingResponse);
            } else {
                Log.w(TAG, "接続確認:onBillingSetupFinished() error code: " + billingResponse);
            }
        }
        @Override
        public void onBillingServiceDisconnected() {
            Log.w(TAG, "切断:onBillingServiceDisconnected()");
        }
    });
}

@Override
public void onPurchasesUpdated(int responseCode, List<Purchase> purchases) {
    Log.i(TAG, "購入商品:onPurchasesUpdated() response: " + responseCode);
}
編集後のBillingManagerクラス

import android.app.Activity;
import android.content.Context;

import com.android.billingclient.api.PurchasesUpdatedListener;
import android.util.Log;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.Purchase;

import java.util.List;

/**
 * TODO: Implement BillingManager that will handle all the interactions with Play Store
 * (via Billing library), maintain connection to it through BillingClient and cache
 * temporary states/data if needed.
 */
public class BillingManager implements PurchasesUpdatedListener {
    private static final String TAG = "BillingManager";

    private final BillingClient mBillingClient;
    private final Activity mActivity;

    public BillingManager(Activity activity) {
        mActivity = activity;
        mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();
        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponse) {
                if (billingResponse == BillingClient.BillingResponse.OK) {
                    Log.i(TAG, "接続確認:onBillingSetupFinished() response: " + billingResponse);
                } else {
                    Log.w(TAG, "接続確認:onBillingSetupFinished() error code: " + billingResponse);
                }
            }
            @Override
            public void onBillingServiceDisconnected() {
                Log.w(TAG, "切断:onBillingServiceDisconnected()");
            }
        });
    }

    @Override
    public void onPurchasesUpdated(int responseCode, List purchases) {
        Log.i(TAG, "購入商品:onPurchasesUpdated() response: " + responseCode);
    }

    public void startPurchaseFlow(String skuId, String billingType) {
        // TODO: Implement launch billing flow here
    }
}

【実行結果】

プログラム編集後、実機でアプリケーションを実行してみると、 Logcat部分に ”接続確認” という文字を確認できます。 onBillingSetupFinished() response: 0 であれば正常に接続完了。 エミュレーターで実行した場合は、 onBillingSetupFinished() error code: 3 と表示されると思います。 エラーコードの詳しい内容については、In-app Billing リファレンスを確認してみてください。

尚、トップ画面の購入ボタン(PURCHASE)を押してみると、下記のようなエラー画面になると思います。

image

これは登録した商品を表示できないというエラー。次の作業は、 Google Play Console に登録されてる商品を画面に表示するプログラムです。

商品データの取得

チュートリアル:5. Get SKUs with details

【編集ファイル: クラス BillingManager】

追加

private static final HashMap<String, List<String>> SKUS;
static
{
    SKUS = new HashMap<>();
    SKUS.put(SkuType.INAPP, Arrays.asList("gas", "premium"));
    SKUS.put(SkuType.SUBS, Arrays.asList("gold_monthly", "gold_yearly"));
}

public List<String> getSkus(@SkuType String type) {
   return SKUS.get(type);
}
public void querySkuDetailsAsync(@BillingClient.SkuType final String itemType,
        final List<String> skuList, final SkuDetailsResponseListener listener) {
    SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder().setSkusList(skuList)
            .setType(itemType).build();
    mBillingClient.querySkuDetailsAsync(skuDetailsParams,
            new SkuDetailsResponseListener() {
                @Override
                public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {
                    listener.onSkuDetailsResponse(responseCode, skuDetailsList);
                }
            });
}
編集後のBillingManagerクラス

import android.app.Activity;
import android.content.Context;
import com.android.billingclient.api.PurchasesUpdatedListener;
import android.util.Log;

import com.android.billingclient.api.BillingClient.BillingResponse;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * BillingManager that handles all the interactions with Play Store
 * (via Billing library), maintain connection to it through BillingClient and cache
 * temporary states/data if needed.
 */
public class BillingManager implements PurchasesUpdatedListener {
    private static final String TAG = "BillingManager";

    private final BillingClient mBillingClient;
    private final Activity mActivity;

    // Defining SKU constants from Google Play Developer Console
    private static final HashMap> SKUS;
    static
    {
        SKUS = new HashMap<>();
        SKUS.put(SkuType.INAPP, Arrays.asList("gas", "premium"));
        SKUS.put(SkuType.SUBS, Arrays.asList("gold_monthly", "gold_yearly"));
    }

    private static final String SUBS_SKUS[] = {"gold_monthly", "gold_yearly"};

    public BillingManager(Activity activity) {
        mActivity = activity;
        mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();
        startServiceConnection(null);
    }

    @Override
    public void onPurchasesUpdated(int responseCode, List purchases) {
        Log.i(TAG, "購入商品:onPurchasesUpdated() response: " + responseCode);
    }

    private void startServiceConnection(final Runnable executeOnSuccess) {
        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(@BillingResponse int billingResponse) {
                Log.i(TAG, "接続確認:onBillingSetupFinished() response: " + billingResponse);
            }
            @Override
            public void onBillingServiceDisconnected() {
                Log.w(TAG, "切断:onBillingServiceDisconnected()");
            }
        });
    }

    public void querySkuDetailsAsync(@BillingClient.SkuType final String itemType,
            final List skuList, final SkuDetailsResponseListener listener) {
        SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder().setSkusList(skuList)
                .setType(itemType).build();
        mBillingClient.querySkuDetailsAsync(skuDetailsParams,
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(int responseCode, List skuDetailsList) {
                        listener.onSkuDetailsResponse(responseCode, skuDetailsList);
                    }
                });
    }

    public List getSkus(@SkuType String type) {
        return SKUS.get(type);
    }

    public void startPurchaseFlow(String skuId, String billingType) {
        // TODO: Implement launch billing flow here
    }
}


取得した商品データをログ出力

チュートリアル:6. Render the SKUs

【編集ファイル: クラス AcquireFragment】

追加

List<String> inAppSkus = mBillingProvider.getBillingManager().getSkus(SkuType.INAPP);
mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.INAPP, inAppSkus,
        new SkuDetailsResponseListener() {
            @Override
            public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {
                if (responseCode == BillingResponse.OK
                        && skuDetailsList != null) {
                    for (SkuDetails details : skuDetailsList) {
                        Log.w(TAG, "商品:Got a SKU: " + details);
                    }
                }
            }
        });
編集後のAcquireFragmentクラス

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClient.BillingResponse;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.codelab.GamePlayActivity;
import com.codelab.sample.R;
import com.codelab.billing.BillingProvider;
import com.codelab.skulist.row.SkuRowData;

import java.util.ArrayList;
import java.util.List;

/**
 * Displays a screen with various in-app purchase and subscription options
 */
public class AcquireFragment extends DialogFragment {
    private static final String TAG = "AcquireFragment";

    private RecyclerView mRecyclerView;
    private SkusAdapter mAdapter;
    private View mLoadingView;
    private TextView mErrorTextView;
    private BillingProvider mBillingProvider;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.acquire_fragment, container, false);
        mErrorTextView = (TextView) root.findViewById(R.id.error_textview);
        mRecyclerView = (RecyclerView) root.findViewById(R.id.list);
        mLoadingView = root.findViewById(R.id.screen_wait);
        // Setup a toolbar for this fragment
        Toolbar toolbar = (Toolbar) root.findViewById(R.id.toolbar);
        toolbar.setNavigationIcon(R.drawable.ic_arrow_up);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });
        toolbar.setTitle(R.string.button_purchase);
        setWaitScreen(true);
        onManagerReady((BillingProvider) getActivity());
        return root;
    }

    /**
     * Refreshes this fragment's UI
     */
    public void refreshUI() {
        Log.d(TAG, "Looks like purchases list might have been updated - refreshing the UI");
        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
    }

    /**
     * Notifies the fragment that billing manager is ready and provides a BillingProvider
     * instance to access it
     */
    public void onManagerReady(BillingProvider billingProvider) {
        mBillingProvider = billingProvider;
        if (mRecyclerView != null) {
            mAdapter = new SkusAdapter(mBillingProvider);
            if (mRecyclerView.getAdapter() == null) {
                mRecyclerView.setAdapter(mAdapter);
                mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
            }
            handleManagerAndUiReady();
        }
    }
    /**
     * Enables or disables "please wait" screen.
     */
    private void setWaitScreen(boolean set) {
        mRecyclerView.setVisibility(set ? View.GONE : View.VISIBLE);
        mLoadingView.setVisibility(set ? View.VISIBLE : View.GONE);
    }

    /**
     * Executes query for SKU details at the background thread
     */
    private void handleManagerAndUiReady() {
        // Start querying for SKUs
        List inAppSkus = mBillingProvider.getBillingManager().getSkus(SkuType.INAPP);
        mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.INAPP, inAppSkus,
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(int responseCode, List skuDetailsList) {
                        if (responseCode == BillingResponse.OK
                                && skuDetailsList != null) {
                            for (SkuDetails details : skuDetailsList) {
                                Log.w(TAG, "商品:Got a SKU: " + details);
                            }
                        }
                    }
                });
        
        // Show the UI
        displayAnErrorIfNeeded();
    }

    private void displayAnErrorIfNeeded() {
        if (getActivity() == null || getActivity().isFinishing()) {
            Log.i(TAG, "エラー:No need to show an error - activity is finishing already");
            return;
        }

        mLoadingView.setVisibility(View.GONE);
        mErrorTextView.setVisibility(View.VISIBLE);
        mErrorTextView.setText(getText(R.string.error_codelab_not_finished));

        // TODO: Here you will need to handle various respond codes from BillingManager
    }
}


実行結果

初期画面および PURCHASEボタンを押した後の画面は今までと同じですが、 PURCHASEボタン押し後の Logcatには登録商品の情報が表示されています。 これで Google Play Console に登録されている商品情報を端末側で取得できたことが確認できますね。後はこの取得できた商品データを、画面上に表示できれば OK でしょう。

商品を画面に表示

【編集ファイル: クラス AcquireFragment】

追加

if (responseCode == BillingResponse.OK && skuDetailsList != null) {
    List<SkuRowData> inList = new ArrayList<>();
    for (SkuDetails details : skuDetailsList) {
        Log.i(TAG, "商品データ:Found sku: " + details);
        inList.add(new SkuRowData(details.getSku(), details.getTitle(), details.getPrice(),
                details.getDescription(), details.getType()));
    }
    if (inList.size() == 0) {
        displayAnErrorIfNeeded();
    } else {
        mAdapter.updateData(inList);
        setWaitScreen(false);
    }
}
編集後のAcquireFragmentクラス

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClient.BillingResponse;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.codelab.sample.R;
import com.codelab.billing.BillingProvider;
import com.codelab.skulist.row.SkuRowData;

import java.util.ArrayList;
import java.util.List;

/**
 * Displays a screen with various in-app purchase and subscription options
 */
public class AcquireFragment extends DialogFragment {
    private static final String TAG = "AcquireFragment";

    private RecyclerView mRecyclerView;
    private SkusAdapter mAdapter;
    private View mLoadingView;
    private TextView mErrorTextView;
    private BillingProvider mBillingProvider;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.acquire_fragment, container, false);
        mErrorTextView = (TextView) root.findViewById(R.id.error_textview);
        mRecyclerView = (RecyclerView) root.findViewById(R.id.list);
        mLoadingView = root.findViewById(R.id.screen_wait);
        // Setup a toolbar for this fragment
        Toolbar toolbar = (Toolbar) root.findViewById(R.id.toolbar);
        toolbar.setNavigationIcon(R.drawable.ic_arrow_up);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });
        toolbar.setTitle(R.string.button_purchase);
        setWaitScreen(true);
        onManagerReady((BillingProvider) getActivity());
        return root;
    }

    /**
     * Refreshes this fragment's UI
     */
    public void refreshUI() {
        Log.d(TAG, "Looks like purchases list might have been updated - refreshing the UI");
        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
    }

    /**
     * Notifies the fragment that billing manager is ready and provides a BillingProvider
     * instance to access it
     */
    public void onManagerReady(BillingProvider billingProvider) {
        mBillingProvider = billingProvider;
        if (mRecyclerView != null) {
            mAdapter = new SkusAdapter(mBillingProvider);
            if (mRecyclerView.getAdapter() == null) {
                mRecyclerView.setAdapter(mAdapter);
                mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
            }
            handleManagerAndUiReady();
        }
    }
    /**
     * Enables or disables "please wait" screen.
     */
    private void setWaitScreen(boolean set) {
        mRecyclerView.setVisibility(set ? View.GONE : View.VISIBLE);
        mLoadingView.setVisibility(set ? View.VISIBLE : View.GONE);
    }

    /**
     * Executes query for SKU details at the background thread
     */
    private void handleManagerAndUiReady() {
        // Start querying for SKUs
        List inAppSkus = mBillingProvider.getBillingManager().getSkus(SkuType.INAPP);
        mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.INAPP, inAppSkus,
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(int responseCode, List skuDetailsList) {
                        if (responseCode == BillingResponse.OK && skuDetailsList != null) {
                            List inList = new ArrayList<>();
                            for (SkuDetails details : skuDetailsList) {
                                Log.i(TAG, "商品データ:Found sku: " + details);
                                inList.add(new SkuRowData(details.getSku(), details.getTitle(), details.getPrice(),
                                        details.getDescription(), details.getType()));
                            }
                            if (inList.size() == 0) {
                                displayAnErrorIfNeeded();
                            } else {
                                mAdapter.updateData(inList);
                                setWaitScreen(false);
                            }
                        }
                    }
                });

        // Show the UI
        displayAnErrorIfNeeded();
    }

    private void displayAnErrorIfNeeded() {
        if (getActivity() == null || getActivity().isFinishing()) {
            Log.i(TAG, "エラー:No need to show an error - activity is finishing already");
            return;
        }

        mLoadingView.setVisibility(View.GONE);
        mErrorTextView.setVisibility(View.VISIBLE);
        mErrorTextView.setText(getText(R.string.error_codelab_not_finished));

        // TODO: Here you will need to handle various respond codes from BillingManager
    }
}


実行結果

Google Play Console から商品データを取得し、ログキャットに表示と inList にデータを追加することで画面上にも商品に関する情報を表示できました。

ただ今回表示できた商品は単品商品(products)のみ。他の商品も画面上に出力するには、インターフェース SkuDetailsResponseListener を利用する必要があります。

全商品を画面に表示

【編集ファイル: クラス AcquireFragment】

編集 handleManagerAndUiReady()内

private void handleManagerAndUiReady() {
    final List<SkuRowData> inList = new ArrayList<>();
    SkuDetailsResponseListener responseListener = new SkuDetailsResponseListener() {
        @Override
        public void onSkuDetailsResponse(int responseCode,
                                         List<SkuDetails> skuDetailsList) {
            if (responseCode == BillingResponse.OK && skuDetailsList != null) {
                // Repacking the result for an adapter
                for (SkuDetails details : skuDetailsList) {
                    Log.i(TAG, "商品データ:Found sku: " + details);
                    inList.add(new SkuRowData(details.getSku(), details.getTitle(),
                            details.getPrice(), details.getDescription(),
                            details.getType()));
                }
                if (inList.size() == 0) {
                    displayAnErrorIfNeeded();
                } else {
                    mAdapter.updateData(inList);
                    setWaitScreen(false);
                }
            }
        }
    };

    // Start querying for in-app SKUs
    List<String> skus = mBillingProvider.getBillingManager().getSkus(SkuType.INAPP);
    mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.INAPP, skus, responseListener);
    // Start querying for subscriptions SKUs
    skus = mBillingProvider.getBillingManager().getSkus(SkuType.SUBS);
    mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.SUBS, skus, responseListener);
}
編集後のAcquireFragmentクラス

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClient.BillingResponse;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.codelab.sample.R;
import com.codelab.billing.BillingProvider;
import com.codelab.skulist.row.SkuRowData;

import java.util.ArrayList;
import java.util.List;

/**
 * Displays a screen with various in-app purchase and subscription options
 */
public class AcquireFragment extends DialogFragment {
    private static final String TAG = "AcquireFragment";

    private RecyclerView mRecyclerView;
    private SkusAdapter mAdapter;
    private View mLoadingView;
    private TextView mErrorTextView;
    private BillingProvider mBillingProvider;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.acquire_fragment, container, false);
        mErrorTextView = (TextView) root.findViewById(R.id.error_textview);
        mRecyclerView = (RecyclerView) root.findViewById(R.id.list);
        mLoadingView = root.findViewById(R.id.screen_wait);
        // Setup a toolbar for this fragment
        Toolbar toolbar = (Toolbar) root.findViewById(R.id.toolbar);
        toolbar.setNavigationIcon(R.drawable.ic_arrow_up);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });
        toolbar.setTitle(R.string.button_purchase);
        setWaitScreen(true);
        onManagerReady((BillingProvider) getActivity());
        return root;
    }

    /**
     * Refreshes this fragment's UI
     */
    public void refreshUI() {
        Log.d(TAG, "Looks like purchases list might have been updated - refreshing the UI");
        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
    }

    /**
     * Notifies the fragment that billing manager is ready and provides a BillingProvider
     * instance to access it
     */
    public void onManagerReady(BillingProvider billingProvider) {
        mBillingProvider = billingProvider;
        if (mRecyclerView != null) {
            mAdapter = new SkusAdapter(mBillingProvider);
            if (mRecyclerView.getAdapter() == null) {
                mRecyclerView.setAdapter(mAdapter);
                mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
            }
            handleManagerAndUiReady();
        }
    }
    /**
     * Enables or disables "please wait" screen.
     */
    private void setWaitScreen(boolean set) {
        mRecyclerView.setVisibility(set ? View.GONE : View.VISIBLE);
        mLoadingView.setVisibility(set ? View.VISIBLE : View.GONE);
    }

    /**
     * Executes query for SKU details at the background thread
     */
    private void handleManagerAndUiReady() {
        final List inList = new ArrayList<>();
        SkuDetailsResponseListener responseListener = new SkuDetailsResponseListener() {
            @Override
            public void onSkuDetailsResponse(int responseCode,
                                             List skuDetailsList) {
                if (responseCode == BillingResponse.OK && skuDetailsList != null) {
                    // Repacking the result for an adapter
                    for (SkuDetails details : skuDetailsList) {
                        Log.i(TAG, "商品データ:Found sku: " + details);
                        inList.add(new SkuRowData(details.getSku(), details.getTitle(),
                                details.getPrice(), details.getDescription(),
                                details.getType()));
                    }
                    if (inList.size() == 0) {
                        displayAnErrorIfNeeded();
                    } else {
                        mAdapter.updateData(inList);
                        setWaitScreen(false);
                    }
                }
            }
        };

        // Start querying for in-app SKUs
        List skus = mBillingProvider.getBillingManager().getSkus(SkuType.INAPP);
        mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.INAPP, skus, responseListener);
        // Start querying for subscriptions SKUs
        skus = mBillingProvider.getBillingManager().getSkus(SkuType.SUBS);
        mBillingProvider.getBillingManager().querySkuDetailsAsync(SkuType.SUBS, skus, responseListener);
    }

    private void displayAnErrorIfNeeded() {
        if (getActivity() == null || getActivity().isFinishing()) {
            Log.i(TAG, "エラー:No need to show an error - activity is finishing already");
            return;
        }

        mLoadingView.setVisibility(View.GONE);
        mErrorTextView.setVisibility(View.VISIBLE);
        mErrorTextView.setText(getText(R.string.error_codelab_not_finished));

        // TODO: Here you will need to handle various respond codes from BillingManager
    }
}


実行結果

Google Play Console から読み込む商品情報の取り扱い方を変えることによって、購読商品の情報も取得でき、画面上に全商品を表示できたことが確認できます。

後は表示されている商品の BUY ボタンを有効化ができれば、購入プロセス完了ですね。

購入ボタンの有効化

チュートリアル: 7. Start a purchase flow

【編集ファイル: クラス BillingManager】

追加

BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
        .setType(billingType)
        .setSku(skuId)
        .build();
mBillingClient.launchBillingFlow(mActivity, billingFlowParams);
編集後のBillingManagerクラス

import android.app.Activity;

import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.PurchasesUpdatedListener;
import android.util.Log;

import com.android.billingclient.api.BillingClient.BillingResponse;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * BillingManager that handles all the interactions with Play Store
 * (via Billing library), maintain connection to it through BillingClient and cache
 * temporary states/data if needed.
 */
public class BillingManager implements PurchasesUpdatedListener {
    private static final String TAG = "BillingManager";

    private final BillingClient mBillingClient;
    private final Activity mActivity;

    // Defining SKU constants from Google Play Developer Console
    private static final HashMap> SKUS;
    static
    {
        SKUS = new HashMap<>();
        SKUS.put(SkuType.INAPP, Arrays.asList("gas", "premium"));
        SKUS.put(SkuType.SUBS, Arrays.asList("gold_monthly", "gold_yearly"));
    }

    private static final String SUBS_SKUS[] = {"gold_monthly", "gold_yearly"};

    public BillingManager(Activity activity) {
        mActivity = activity;
        mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();
        mBillingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(@BillingResponse int billingResponse) {
                Log.i(TAG, "接続確認:onBillingSetupFinished() response: " + billingResponse);
            }
            @Override
            public void onBillingServiceDisconnected() {
                Log.w(TAG, "接続確認:onBillingServiceDisconnected()");
            }
        });
    }

    @Override
    public void onPurchasesUpdated(@BillingResponse int responseCode, List purchases) {
        Log.i(TAG, "購入商品:onPurchasesUpdated() response: " + responseCode);
    }

    public void querySkuDetailsAsync(@BillingClient.SkuType final String itemType,
                                     final List skuList, final SkuDetailsResponseListener listener) {
        SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder()
                .setSkusList(skuList).setType(itemType).build();
        mBillingClient.querySkuDetailsAsync(skuDetailsParams,
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(int responseCode,
                                                     List skuDetailsList) {
                        listener.onSkuDetailsResponse(responseCode, skuDetailsList);
                    }
                });
    }

    public List getSkus(@SkuType String type) {
        return SKUS.get(type);
    }

    public void startPurchaseFlow(String skuId, String billingType) {
        BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
                .setType(billingType)
                .setSku(skuId)
                .build();
        mBillingClient.launchBillingFlow(mActivity, billingFlowParams);
    }
}


実行結果

商品一覧の BUYボタンを押すと、商品に関する詳細情報が表示。そして購入ボタンを押すと、支払いが完了。支払いがが完了すると、メールが届いて購入商品の内容を確認することができます。 こういった感じで Google Play Console に登録されてる商品を、アプリ側の方で表示・操作することが可能。

また定期購入商品については、利用期間中に再度購入ボタンを押すと、解約のアナウンスが自動的に表示。単品商品については、購入後消費プロセスのステップを踏まないと再度購入することはできません。 あと購入プロセスの途中で接続が切れた場合の処理内容については、チュートリアルの8番目にプログラムが紹介されています。

最新のアプリ開発スキルが身に付く

CodeCampの無料体験はこちら

まとめ

今回はチュートリアルに沿って、あらかじめ登録されている商品をアプリ側で表示するという内容でした。

複雑な処理が伴う課金アプリの実装も、このようなシンプルなプロセスを体験することで、複雑な仕組みが解きほぐされて、徐々に理解していけるようになると思います。

ただし、サンプルプログラムには Java の応用的な構文も登場。連想配列(HashMap<>)やインタフェース、各メソッドの引数設定など Java の基礎を理解しておかないと、分からない項目も。

「儲かるアプリを作りたい」「仕事上アプリを理解する必要がある」という方、参考書や独学もいいですが、「スピード感」と「正確性」のあるプログラミングスクールはいかがでしょうか? CodeCamp では、『オンライン × マンツーマン × 現役エンジニア』 で効率よく学習を進めることができるでしょう。

プログラミングスクールやアプリ開発のこと、未経験の方は一度無料体験・・・・でお試ししてみませんか?詳しくは 公式ホームページ より確認してみて下さい。

関連記事

オシママサラ
この記事を書いた人
オシママサラ
\ 無料体験開催中!/自分のペースで確実に習得!
オンライン・プログラミングレッスンNo.1のCodeCamp