воскресенье, 3 января 2016 г.

Gradle Product Flavors. Подключение библиотек, организация кода, сочетания вариантов

С 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 комментарий:

Михаил Игнатов комментирует...

Спасибо, очень полезная статья