💰
Google Play Billing Library 入門3 アイテムを表示する
Google Play Consoleで設定したアイテムをアプリで表示する
以下ダイアグラムは購入処理のの完成形
今回関係ある個所は
- MainActivity
- BillingViewModel
- BillingRepository
- initialBillingUseCase
- GetProductUseCase
- BillingRepository
- ProductDataSorce
- BillingClientWrapper
詳細は以下Githubで確認してください
https://github.com/giglancer/SampleBilling
データ層
BillingClientWrapper
BillingLibraryの初期化と接続を行う
@Singleton
class BillingClientWrapper @Inject constructor(
@ApplicationContext private val context: Context
) {
private var _billingClient: BillingClient? = null
val billingClient: BillingClient
get() = _billingClient ?: throw IllegalStateException("Billing client is not initialized.")
val isReady: Boolean
get() = _billingClient?.isReady == true
fun initialize(purchasesUpdatedListener: PurchasesUpdatedListener) {
_billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.build()
)
.enableAutoServiceReconnection()
.build()
}
suspend fun connect(): Result<Unit> = suspendCancellableCoroutine { continuation ->
if (_billingClient == null) {
continuation.resume(Result.failure(Exception("BillingClient not initialized")))
return@suspendCancellableCoroutine
}
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
// 自動再接続が有効
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
continuation.resume(Result.success(Unit))
} else {
continuation.resume(Result.failure(Exception(billingResult.debugMessage)))
}
}
})
}
fun disconnect() {
_billingClient?.endConnection()
_billingClient = null
}
}
ProductDataSource
Google Play Consoleで設定したアイテムのアイテムのIDを渡してアイテム情報を取得する
@Singleton
class ProductDataSource @Inject constructor(
private val billingClientWrapper: BillingClientWrapper
) {
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun queryProducts(
productIds: List<String>,
productType: String = BillingClient.ProductType.INAPP
): Result<List<ProductDetails>> {
if (!billingClientWrapper.isReady) {
return Result.failure(Exception("Billing client is not ready"))
}
return suspendCancellableCoroutine { continuation ->
val productList = productIds.map { productId ->
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(productType)
.build()
}
val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
billingClientWrapper.billingClient.queryProductDetailsAsync(params) { billingResult, queryProductDetailsResult ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
continuation.resume(
Result.success(queryProductDetailsResult.productDetailsList)
)
} else {
continuation.resume(
Result.failure(Exception(billingResult.debugMessage))
)
}
}
}
}
}
BillingViewModel
viewModelの初期化と同時にBillingLibraryと接続する
仮でローカルで持っているアイテムのIDを使用してBillingサーバーに問い合わせする
_billingUiStateで保持している products: List<ProductDetailsをUIで使用すればアイテムリストが表示される
@Singleton
class BillingRepositoryImpl @Inject constructor(
private val billingClientWrapper: BillingClientWrapper,
private val productDataSource: ProductDataSource,
) : BillingRepository {
private val _connectionState = MutableStateFlow<BillingConnectionState>(BillingConnectionState.Disconnected)
override val connectionState: Flow<BillingConnectionState> = _connectionState.asStateFlow()
override val purchaseUpdate: Flow<PurchaseUpdate> = purchaseDataSource.observePurchaseUpdates()
override suspend fun initialize(): Result<Unit> {
_connectionState.value = BillingConnectionState.Connecting
return billingClientWrapper.connect()
.onSuccess {
_connectionState.value = BillingConnectionState.Connected
}
.onFailure {
_connectionState.value = BillingConnectionState.Error(
it.message ?: "Unknown error"
)
}
}
override suspend fun getProducts(productIds: List<String>): Result<ProductList> {
return productDataSource.queryProducts(productIds)
.map { products ->
ProductList(products = products)
}
}
override fun disconnect() {
billingClientWrapper.disconnect()
_connectionState.value = BillingConnectionState.Disconnected
}
}
ドメイン層はシンプルなインターフェースなので省略
プレゼンテーション層
@HiltViewModel
class BillingViewModel @Inject constructor(
private val initializeBillingUseCase: InitializeBillingUseCase,
private val getProductsUseCase: GetProductsUseCase,
) : ViewModel() {
private val _billingUiState = MutableStateFlow(BillingUiState(status = Status.Loading))
val billingUiState: StateFlow<BillingUiState> = _billingUiState.asStateFlow()
val notConsumedPurchases: List<String>
get() = listOf("test1", "test5")
val consumedPurchases: List<String>
get() = listOf("test2", "test3", "test4")
init {
initializeBilling()
}
private fun initializeBilling() {
viewModelScope.launch {
_billingUiState.value = BillingUiState(status = Status.Loading)
initializeBillingUseCase()
.onSuccess {
Log.d("BillingViewModel", "Billing client connected")
loadPurchases()
loadProducts()
}
.onFailure {
Log.d("BillingViewModel", "Billing client connection failed")
_billingUiState.value =
_billingUiState.value.copy(status = Status.Error("Billingクライアントの接続に失敗しました"))
}
}
}
private fun loadProducts() {
viewModelScope.launch {
val productIds = notConsumedPurchases + consumedPurchases
getProductsUseCase(productIds)
.onSuccess {
Log.d("BillingViewModel", "Products loaded: ${it.products}")
_billingUiState.value = _billingUiState.value.copy(
status = Status.Success,
products = it.products,
)
}
.onFailure {
Log.d("BillingViewModel", "Failed to load products: ${it.message}")
_billingUiState.value =
_billingUiState.value.copy(status = Status.Error("商品の読み込みに失敗しました"))
}
}
}
}
Discussion