С free и premium разобрались, теперь усложним задачу. Пусть наше приложение распространяется на нескольких площадках (Google Play, Samsung Apps и Amazon) и в нём есть встроенные покупки. Хорошо бы также сделать, чтобы библиотеки подключались только для нужной сборки, то есть чтобы версия для Google Play не тянула с собой логику самсунга. Ну и конечно, для каждой площадки мы должны собирать и free, и premium.
Страшно?
А всего-то что надо сделать:
- Включение/отключение библиотек
- Добавление в разные сборки специфической логики
- Как-то сочетать несколько flavors
Кому лень читать буквы, пожалуйте сразу в код.
Включение зависимостей в разные вариации
Для начала добавим в билд-скрипт несколько вариаций:
app/build.gradle
    productFlavors {
        premium {
            applicationId "com.demos.productflavors.premium"
        }
        free {
            applicationId "com.demos.productflavors"
        }
        google { }
        samsung { }
        amazon { }
    }
Версии для Google Play дополнительных библиотек не требуется. Для самсунговского биллинга нужно подключить дополнительный проект iap3Helper, а для амазоновского — добавить jar-библиотеку. Делается это так:
app/build.gradle
dependencies {
    compile fileTree(dir: 'libs/common', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.0'
    compile 'com.android.support:design:23.1.0'
    amazonCompile fileTree(dir: 'libs/amazon', include: ['*.jar'])
    samsungCompile project(':iap3Helper')
}
Таким образом, специфические либы попадут только в те вариации, в которых нужны. Подключение библиотек из репозиториев работает точно так же. Также стоит обратить внимание на организацию папки libs: в ней есть отдельная директория common, где лежат либы для всех вариаций (в моей демке, правда, там пусто, но вдруг понадобится), и amazon, где всё только для амазона.
Добавление в разные сборки специфической логики
Организация проекта получается примерно такой (названия пакетов для краткости опущены):
/src
    /main
        /java
            MainActivity.java
            PurchaseFlow.java
            IabListener.java
        /res
        AndroidManifest.xml
    /amazon
        /java
            AmazonPurchaseFlow.java
            PurchaseFlowFactory.java
        /res
        AndroidManifest.xml
    /google
        /java
            GooglePurchaseFlow.java
            PurchaseFlowFactory.java
        /res
        AndroidManifest.xml
    /samsung
        /java
            SamsungPurchaseFlow.java
            PurchaseFlowFactory.java
        /res
        AndroidManifest.xml
В main мы определяем базовый класс PurchaseFlow с методом purchase. В каждую вариацию добавляем по наследнику этого класса и реализуем там логика покупки. Также во всех вариациях присутствует по отдельному классу PurchaseFlowFactory с примерно таким содержанием:
amazon/PurchaseFlowFactory.java
public class PurchaseFlowFactory { public static PurchaseFlow createPurchaseFlow(Activity activity, IabListener iabListener) { return new AmazonPurchaseFlow(activity, iabListener); } }
(В main такой класс не нужен. Во-первых, компилятор выдаст ошибку Duplicate class, а во-вторых и писать там особенно нечего)
Работают ин-аппы по-разному, у всех у них свой жизненный цикл, инициализация и коллбэки. Это нужно унифицировать, поэтому в PurchaseFlow появляются всякие onActivityResult и onActivityCreate. Также нужен и универсальный коллбэк, для этого в main определён IabListener. Не буду вдаваться в подробности архитектуры, она у каждого своя. Главное, что мы должным образом организовали код, и теперь можем в MainActivity написать примерно следующее:
MainActivity.java
public class MainActivity extends AppCompatActivity { private PurchaseFlow mPurchaseFlow; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mPurchaseFlow = PurchaseFlowFactory.createPurchaseFlow(this, mIabListener); mPurchaseFlow.onActivityCreate(this); … } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (mPurchaseFlow != null) { mPurchaseFlow.onActivityResult(requestCode, resultCode, data); } } public void startPurchaseFlow(View view) { mPurchaseFlow.purchase(getString(R.string.sku)); } private IabListener mIabListener = new IabListener() { @Override public void onServiceUnavailable() { DPLog.w("InApp billing service unavailable"); } @Override public void onPurchaseFinished() { DPLog.d("Purchase finished"); Toast.makeText(MainActivity.this, "Purchase finished", Toast.LENGTH_SHORT).show(); } @Override public void onPurchaseFailed() { DPLog.e("Purchase failed"); Toast.makeText(MainActivity.this, "Purchase failed", Toast.LENGTH_SHORT).show(); } }; }
При сборке вариации подхватится определённый именно для неё PurchaseFlowFactory, так что при исполнении кода отработает нужная логика ин-аппа.
Дополнение манифеста
Для корректной работы встроенной покупки зачастую надо добавить приложению то разрешение, то какой-то компонент из библиотеки (активность, ресивер или сервис). Полностью дублировать манифест в каждой вариации не нужно, достаточно описать недостающие части. При сборке манифесты смержатся, и получится то, что надо. Например, версии для Google Play не нужно ничего, кроме дополнительного разрешения, и её манифест будет выглядеть всего лишь так:
google/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="com.android.vending.BILLING" /> </manifest>
Сочетания вариаций
А ведь кроме google/amazon/samsung у нас есть вариации free и premium. При нынешней конфигурации билд-скрипта мы можем собрать 10 сборок (дебажная и релизная для каждой из пяти вариаций), а надо бы 12 (googleFreeDebug, amazonPremiumRelease и т.д.)
На этот случай в билд-скрипте предусмотрена штука под названием flavor dimensions. По смыслу это что-то вроде «типа вариации». Мы должны определить их список, а потом указать для каждой вариации её тип. Получается примерно так:
app/build.gradle
android {
    …
    flavorDimensions "version", "inapp"
    productFlavors {
        premium {
            applicationId "com.demos.productflavors.premium"
            dimension "version"
        }
        free {
            applicationId "com.demos.productflavors"
            dimension "version"
        }
        google {
            dimension "inapp"
        }
        samsung {
            dimension "inapp"
        }
        amazon {
            dimension "inapp"
        }
    }
}
Теперь билд-скрипт способен собрать нужные нам 12 сборок.
Application Id для сочетания вариаций
При нынешней конфигурации билда у всех free-версий айдишник будет com.demos.productflavors, а у всех premium — com.demos.productflavors.premium. Бывает, что этого недостаточно, и хочется сделать разные applicationId ещё и для каждого стора. Стандартных средств для этого нет, но можно написать скрипт:
app/build.gradle
def baseAppId = "com.demos.productflavors"
android {
    …
    flavorDimensions "version", "inapp"
    productFlavors {
        …
    }
    applicationVariants.all { variant ->
        def mergedFlavor = variant.mergedFlavor
        def flavors = variant.productFlavors
        def appId = baseAppId
        for (int i = 0; i < flavors.size(); i++) {
            appId += "."
            appId += flavors[i].name
        }
        mergedFlavor.setApplicationId(appId);
    }
}
Как несложно догадаться, этот скрипт просто добавляет в application id названия вариаций через точку. В итоге у нас получатся айдишники вида com.demos.productflavors.free.samsung. Порядок следования названий вариаций зависит от порядка их типов в flavorDimensions: если его поменять, будет com.demos.productflavors.samsung.free.
Можно написать скрипт по-другому:
applicationVariants.all { variant ->
    def mergedFlavor = variant.mergedFlavor
    switch (variant.flavorName) {
        case "amazonFree":
            mergedFlavor.setApplicationId("com.demos.productflavors.amazon.free")
            break
        case "amazonPremium":
            mergedFlavor.setApplicationId("com.demos.productflavors.amazon.premium")
            break
        …
    }
}
Так будет подлиннее, но зато можно более гибко настраивать айдишники, если есть такое желание.
А как вообще собирать?
Можно просто в IDE, выбрав нужный Build Variant.
А можно из командной строки примерно так:
./gradlew assembleSamsungPremiumDebug
Очевидно, в результате получится дебажная премиальная сборка для Samsung.
Можно собирать одной командой сразу несколько apk-шек
./gradlew assembleSamsungPremiumDebug assembleSamsungPremiumRelease
А можно собрать сразу все возможные варианты сборок:
./gradlew assemble
Всё
На этом пока всё. Есть ещё интересная штука Manifest Placeholders, я обязательно о них напишу, как только придумаю, зачем они могут быть нужны.
Исходники к обеим частям. Просьба не рассматривать их, как исчерпывающую реализацию In-App billing. Это статья про варианты сборок, а не про применение конкретных библиотек. (Честно говоря, код для покупок написан левой ногой, лишь бы запускалось что-то похожее на правду)
 
1 комментарий:
Спасибо, очень полезная статья
Отправить комментарий