-
Notifications
You must be signed in to change notification settings - Fork 1
Cheat sheet
Boby Tanev edited this page Nov 15, 2016
·
2 revisions
Result: a generated release buildable Android project pushed into GitHub
- create GitHub repo (Android + ignore)
- checkout repo
- generate new Android project
- add in build.gradle
defaultConfig {
...
minSdkVersion 23
targetSdkVersion 23
...
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
- generate signing cert
- update build.gradle
signingConfigs {
release {
storeFile file("release.jks")
storePassword project.hasProperty("releaseStorePass") ? releaseStorePass : "n/a"
keyAlias "j2d"
keyPassword project.hasProperty("releaseKeyPass") ? releaseKeyPass : "n/a"
}
}
buildTypes {
debug {
}
release {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
- talk about the env variables and how we can configure them
- option 1: gradle.properties - auto loaded
- option 2: external git ignored properties file -> How to load it into gradle project?
- introduce getSecret method
def getSecret(def key, def defaultValue = null) {
return project.hasProperty(key) ? project.property(key) : defaultValue
}
- add gradle utils file
apply from: "$rootDir/gradleHelper.gradle"
- copy
getSecret
into gradleHelper, show how to "export" the method
/**
* Get the value for the secret key
*
* @param key the key identifier
* @param defaultValue default value of such key is missing
* @return the value behind the key or the defaultValue
*/
def getSecret(def key, def defaultValue = null) {
return project.hasProperty(key) ? project.property(key) : defaultValue
}
// Export methods by turning them into closures
ext {
getSecret = this.&getSecret
}
- talk about option 2 - load properties file method
/**
* Load properties file into the project
* @param fileName properties file name
*/
def loadProperties(def fileName) {
def propertiesFile = file(fileName)
if (!propertiesFile.exists()) {
println("Properties file missing: " + fileName)
return
}
Properties properties = new Properties()
properties.load(propertiesFile.newReader("UTF-8"))
properties.each { property ->
// all extra properties must be set in: project.ext
project.ext.set(property.key, property.value)
}
}
- execure
loadProperties("localSecrets.prperties")
inandroid
block - update git ignore with "localSecrets.prperties"
- build local: debug and release
- push to GitHub
Result: a release build APK that can be downloaded from CircleCI and installed on Device
- go to circleci.com and add the project from GitHub
- add env variables:
ORG_GRADLE_PROJECT_releaseStorePass
ORG_GRADLE_PROJECT_releaseKeyPass
- talk about circleci android build: https://circleci.com/docs/android/
- java8 required for API 24+ build but missing
machine:
environment:
GRADLE_OPTS: -Xmx512m
java:
version: oraclejdk8
- installing additional/update android sdk/libs
dependencies:
pre:
- echo y | android update sdk --no-ui --all --filter tools
- echo y | android update sdk --no-ui --all --filter extra-android-m2repository
- echo y | android update sdk --no-ui --all --filter extra-android-support
- echo y | android update sdk --no-ui --all --filter extra-google-m2repository
- echo y | android update sdk --no-ui --all --filter build-tools-25.0.0
- echo y | android update sdk --no-ui --all --filter android-25
- create
circle.yml
file
machine:
environment:
GRADLE_OPTS: -Xmx512m
java:
version: oraclejdk8
dependencies:
pre:
- echo y | android update sdk --no-ui --all --filter tools
- echo y | android update sdk --no-ui --all --filter extra-android-m2repository
- echo y | android update sdk --no-ui --all --filter extra-android-support
- echo y | android update sdk --no-ui --all --filter extra-google-m2repository
- echo y | android update sdk --no-ui --all --filter build-tools-25.0.0
- echo y | android update sdk --no-ui --all --filter android-25
test:
override:
# assemble app & run unit test(s)
- ./gradlew clean build test -PdisablePreDex -Pcom.android.build.threadPoolSize=4
# copy the build outputs to artifacts
- cp -r app/build/outputs/* $CIRCLE_ARTIFACTS
deployment:
beta:
branch: [develop]
commands:
- ./gradlew assembleDebug
prod:
branch: [master]
commands:
- ./gradlew assembleRelease
- push
circle.yml
and trigger adevelop
build - download apk and install it manually on device
adb install app-release.apk
Result: release APK installable via Crashlytics Beta
- what is Fabric / Crashlytics?
- go to fabric.io and create a team
- copy api key and secret
- create android
Application
class - use the fabric Android studio pluging to add the initial Fabric / Crashlytics integration
- STOP! and remove the hardcoded key from AndroidManifest. Talk about fabric.properties file
- https://docs.fabric.io/android/fabric/settings/working-in-teams.html#android-projects
- fabric.properties
- contains the key and the secret - must not be pushed into the repository(!) what about circleci?
- generate properties file in gradle
-
afterEvaluate
gradle hook - update
localSecrets.prperties
file with fabric key and secret - introduce
buildFabricPropertiesIfNeeded()
method
afterEvaluate {
loadProperties("localSecrets.properties")
buildFabricPropertiesIfNeeded()
}
...
/**
*
* build fabric properties file, if missing
*/
def buildFabricPropertiesIfNeeded() {
def propertiesFile = file("fabric.properties")
if (!propertiesFile.exists()) {
def commentMessage = "This is autogenerated crashlytics property from system environment to prevent key to be committed to source control."
ant.propertyfile(file: "fabric.properties", comment: commentMessage) {
entry(key: "apiSecret", value: getSecret("fabricApiSecret"), operation: "=")
entry(key: "apiKey", value: getSecret("fabricApiKey"), operation: "=")
}
}
}
- update
circle.yml
with crashlytics distribution
deployment:
beta:
branch: [develop]
commands:
- ./gradlew crashlyticsUploadDistributionRelease
prod:
branch: [master]
commands:
- ./gradlew crashlyticsUploadDistributionRelease
- add circleci env variables:
ORG_GRADLE_PROJECT_fabricApiKey
ORG_GRADLE_PROJECT_fabricApiSecret
- add application id suffux for Debug builds:
.debug
- initial crashlytics gradle config
- explicitly enable for Release builds
- explicitly disable for Debug builds - why do we need it do be disabled for debug builds?
debug {
applicationIdSuffix ".debug"
// disable crashlytics
buildConfigField("boolean", "USE_CRASHLYTICS", "false");
ext.enableCrashlytics = false
}
release {
signingConfig signingConfigs.release
// enable crashlytics
buildConfigField("boolean", "USE_CRASHLYTICS", "true");
ext.enableCrashlytics = true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
- edit the Application object - only init Crashlytics when enabled
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.USE_CRASHLYTICS) {
Fabric.with(this, new Crashlytics());
Timber.plant(new CrashReportingTree());
}
}
- push and build
- first build will fail to publish to Beta because the application is not registered
- download APK and run manually
- rebuild -> all successful
Result: The binaries distributed via Crashlytics Beta will have proper build version and dev changelog
- edit
gradle.properties
and add (we'll use those values to generated version code/name):
versionMajor=1
versionMinor=0
versionPatch=0
- edit
build.gradle
and add indefaultConfig
section:
defaultConfig {
...
versionCode getBuildNumber()
versionName getVersionAsString()
...
}
- update
gradleHelper.gradle
with:
/**
*
* @return CircleCI build branch or "localdev"
*/
def getBranchName() {
def branch = System.getenv("CIRCLE_BRANCH")
if (branch?.length() > 0) {
branch = branch.trim()
} else {
// fallback for local dev
branch = "localdev"
}
return branch
}
/**
*
* @return the branch name containing only lowercase alphabetic symbols
*/
def getSafeBranchIdentifier() {
def branch = getBranchName()
// remove all non alphabetic symbols
branch = branch.replaceAll("[^a-zA-Z]", "").toLowerCase()
if ("master".equalsIgnoreCase(branch)) {
// do not generate suffix for "master" branch
return ""
}
return branch
}
/**
*
* @return CI build number or a generated one
*/
def getBuildNumber() {
def circleBranch = System.getenv("CIRCLE_BUILD_NUM")
if (circleBranch?.length() > 0) {
// CircleCI build number
return circleBranch as int
}
// fallback mechanism for non CI env
return (versionMajor.toInteger() * 10000) + (versionMinor.toInteger() * 100) + (versionPatch.toInteger())
}
/**
*
* @return version string identifier (major.minor.buildNumber[-branch])
*/
def getVersionAsString() {
def buildNumber = getBuildNumber()
def branchSuffix = getSafeBranchIdentifier()
if (branchSuffix?.length() > 0) {
branchSuffix = "-$branchSuffix"
} else {
branchSuffix = ""
}
return "$versionMajor.$versionMinor.$buildNumber$branchSuffix"
}
- generate changelog for Crashlytics Beta releases using GitHub commits
// enable crashlytics
buildConfigField("boolean", "USE_CRASHLYTICS", "true");
ext.enableCrashlytics = true
ext.betaDistributionReleaseNotes = getChangelog()
...
/**
*
* @return Generated changelog from the last 10 GitHub commits
*/
def getChangelog() {
if ("localdev".equalsIgnoreCase(getBranchName())) {
return "Local development build"
}
def logCmd = 'git log --oneline --no-decorate -n 10'
def logs = logCmd.execute().text.trim()
def items = []
if (logs.length() > 0) {
def lines = logs.split("\n")
for (int i = 0; i < lines.length; i++) {
items.add(String.format("%02d) %s", i + 1, lines[i]))
}
}
return items.join("\n")
}
- push and build
Result: 3 new product flavors (with different configurations) added to the project: local, beta, prod. Resulting in 6 different builds 3 * (Debug, Release)
- add three new product flavors in the build.gradle
productFlavors {
localdev {
}
beta {
}
prod {
}
}
- sync and observe changes
- do a build gradle refactoring to enable different product flavors:
- app names (remove
app_name
fromstrings
first) - application identifiers (packages) (dependent on branch and flavor)
- crashlytics to be enabled only on local flavor
android {
// explicitly load local secrets here
loadProperties("localSecrets.properties")
compileSdkVersion 25
buildToolsVersion "25.0.0"
def applicationIdentifier = "com.java2days.j2ddemo"
def applicationName = "J2D Demo"
defaultConfig {
applicationId applicationIdentifier
minSdkVersion 23
targetSdkVersion 23
versionCode getBuildNumber()
versionName getVersionAsString()
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// enable crashlytics
buildConfigField("boolean", "USE_CRASHLYTICS", "true");
ext.enableCrashlytics = true
ext.betaDistributionReleaseNotes = getChangelog()
// app name
resValue "string", "app_name", applicationName
}
productFlavors {
def branchSuffix = getSafeBranchIdentifier()
// flavor specific identifier
def flavorApplicationId = "${applicationIdentifier}${branchSuffix?.length() > 0 ? "." : "" }${branchSuffix}"
// flavor specific app name
def flavorApplicationName = "${applicationName}${branchSuffix?.length() > 0 ? "-" : "" }${branchSuffix}"
localdev {
applicationId "${flavorApplicationId}.local"
// disable Crashlytics for local builds
buildConfigField("boolean", "USE_CRASHLYTICS", "false");
ext.enableCrashlytics = false
// app name
resValue "string", "app_name", flavorApplicationName
}
beta {
// base package.<branch>.beta
applicationId "${flavorApplicationId}.beta"
// app name
resValue "string", "app_name", "$flavorApplicationName Beta"
}
prod {
applicationId flavorApplicationId
}
}
signingConfigs {
release {
storeFile file("keystore.jks")
storePassword getSecret("releaseStorePass", "n/a")
keyAlias "j2d"
keyPassword getSecret("releaseKeyPass", "n/a")
}
}
buildTypes {
debug {
// explicitly change the package of the debug builds
applicationIdSuffix ".debug"
}
release {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}
- update
circle.yml
deployment:
beta:
branch: [develop]
commands:
- ./gradlew crashlyticsUploadDistributionBetaRelease
prod:
branch: [master]
commands:
- ./gradlew crashlyticsUploadDistributionProdRelease
- push and build
- first build will fail to publish to Beta because the application is not registered
- download APK and run manually
- rebuild -> all successful
Result: debug builds only dev helpful libraries added. Stetho. Also non fatal exceptions and logs will be sync'd in Crashlytics
- create
debug
variant directory undersrc
and add: - debug Application class
- debug AndroidManifest
public class J2DDemoDebugApplication extends J2DDemoApplication {
@Override
public void onCreate() {
super.onCreate();
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.java2days.j2ddemo">
<application
tools:replace="android:name"
android:name=".J2DDemoDebugApplication" />
</manifest>
- in
build.grade
add the following dependencies (Timber (logging) and Stetho as debug only)
dependencies {
compile 'com.jakewharton.timber:timber:4.3.1'
// debug lib
debugCompile 'com.facebook.stetho:stetho:1.4.1'
debugCompile 'com.facebook.stetho:stetho-timber:1.4.1'
debugCompile 'com.facebook.stetho:stetho-urlconnection:1.4.1'
// debugCompile 'com.facebook.stetho:stetho-okhttp3:1.4.1'
}
- in the debug Application class init Stetho and debug Timber tree
@Override
public void onCreate() {
super.onCreate();
// init Stetho
Stetho.initializeWithDefaults(this);
// init debug Timber
Timber.plant(new Timber.DebugTree());
Timber.plant(new StethoTree());
}
- edit main Application class. Add special crashlytics timber tree to allow logs and non fatals syncing
public class J2DDemoApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.USE_CRASHLYTICS) {
Fabric.with(this, new Crashlytics());
Timber.plant(new CrashReportingTree());
}
}
private static class CrashReportingTree extends Timber.Tree {
@Override
protected boolean isLoggable(String tag, int priority) {
return priority >= Log.INFO;
}
@Override protected void log(int priority, String tag, String message, Throwable t) {
Crashlytics.log(priority, tag, message);
if (t != null) {
// log non fatal exception
Crashlytics.logException(t);
}
}
}
}
- demo time: show how Stetho works in chrome
- demo time: show crashlytics exceptions and non fatals