Skip to content

Instantly share code, notes, and snippets.

@marukami
Created October 17, 2017 10:25
Show Gist options
  • Save marukami/2a72e2315423ec314cc53bd98838129e to your computer and use it in GitHub Desktop.
Save marukami/2a72e2315423ec314cc53bd98838129e to your computer and use it in GitHub Desktop.
Android Quality Matters - Part 1: Build Foundations

Shared Dependencies Versions

// dependencies.gradle
ext.versions = [
    code               : 1,
    name               : '1.0',

    minSdk             : 16,
    targetSdk          : 26,
    compileSdk         : 26,
    buildTools         : '26.0.2',
    kotlin             : '1.1.51',
    androidGradlePlugin: '3.0.0-rc1',
    supportLibs        : '26.1.0',
    constraintLayout   : '1.0.2',
    junit              : '4.12',
    espresso           : '3.0.1',
    supportTestRunner  : '1.0.0',
]

Shared Dependencies

// dependencies.gradle
ext.versions = [ … ]

ext.gradlePlugins = [
    android: "com.android.tools.build:gradle:$versions.androidGradlePlugin",
    kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
]

ext.libraries = [
    kotlin           : "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin",
    supportAppCompat : "com.android.support:appcompat-v7:$versions.supportLibs",
    constraintLayout : 
    "com.android.support.constraint:constraint-layout:$versions.constraintLayout",
    junit            : "junit:junit:$versions.junit",
    supportTestRunner: "com.android.support.test:runner:$versions.supportTestRunner",
    espressoCore     : 
    "com.android.support.test.espresso:espresso-core:$versions.espresso",
]

Applying Dependencies

apply from: 'dependencies.gradle'

buildscript {
    // Gradle will not find vars defined in an external file when referring to them
    // in the buildscript block, unless you link it from the buildscript block, too.
    apply from: 'dependencies.gradle'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath gradlePlugins.android

        classpath gradlePlugins.kotlin
    }
}

Applying Dependencies

apply from: 'dependencies.gradle'

buildscript {
    // Gradle will not find vars defined in an external file when referring to them
    // in the buildscript block, unless you link it from the buildscript block, too.
    apply from: 'dependencies.gradle'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath gradlePlugins.android

        classpath gradlePlugins.kotlin
    }
}

Applying Dependencies

// apply from: 'dependencies.gradle' automatically added
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    // the ext.versions in the dependencies.gradle is accessable
    compileSdkVersion versions.compileSdk
    buildToolsVersion versions.buildTools
}

Applying Dependencies

android {
   defaultConfig {
        applicationId "com.domain.appname"
        minSdkVersion versions.minSdk
        targetSdkVersion versions.targetSdk
        versionCode versions.code
        versionName versions.name
    }
}

Applying Dependencies

android {
   defaultConfig {
        applicationId "com.domain.appname"
        minSdkVersion versions.minSdk
        targetSdkVersion versions.targetSdk
        versionCode versions.code
        versionName versions.name
    }
}

Applying Dependencies

dependencies {
    implementation libraries.kotlin
    implementation libraries.supportAppCompat
    implementation libraries.constraintLayout
}
dependencies {
    testImplementation libraries.junit
}
dependencies {
    androidTestImplementation libraries.supportTestRunner
    androidTestImplementation libraries.espressoCore
}

Common Package Exclude

packagingOptions {
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/NOTICE.txt'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/LICENSE.txt'
    exclude 'META-INF/services/javax.annotation.processing.Processor'
}

Fail Fast

lintOptions {
    warningsAsErrors true
    abortOnError true // Fail early.
    // Okio references java.nio that does not presented in Android SDK.
    disable 'InvalidPackage' 
}

Kotlin Lint Issue

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) 
    }
}

Kotlin Lint Workaround

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) 
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<lint>
    <issue id="UnusedResources">
        <ignore path="res/layout/activity_main.xml"/>
    </issue>
</lint>

Plugins - Dex Count

inline


Dex Count

// dependencies.gradle
ext.versions = [
    dexcountPlugin     : '0.8.1',
]
ext.gradlePlugins = [
    dexcount: "com.getkeepsafe.dexcount" +
    ":dexcount-gradle-plugin:$versions.dexcountPlugin",
]

Dex Count

dependencies {
  classpath gradlePlugins.android

  classpath gradlePlugins.kotlin

  classpath gradlePlugins.dexcount
}

Dependency Update Checker

// dependencies.gradle
ext.versions = [
  versionsGradlePlugin: '0.15.0',
]
ext.gradlePlugins = [
  versions: "com.github.ben-manes:" +
  "gradle-versions-plugin:$versions.versionsGradlePlugin",
]

Dependency Update Checker

// root/build.gradle
buildscript {
    …
    dependencies {
        …
        classpath gradlePlugins.versions
    }
}

Dependency Update Checker

// root/build.gradle
buildscript {
    …
    dependencies {
        …
        classpath gradlePlugins.versions
    }
}

./gradew dependencyUpdates

# The following dependencies are using the latest milestone version:
#  - com.android.support:appcompat-v7:26.1.0
#  - org.jetbrains.kotlin:kotlin-annotation-processing:1.1.51
#  - org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.51

# The following dependencies have later milestone versions:
#  - org.jacoco:org.jacoco.agent [0.7.4.201502262128 -> 0.7.9]
#  - org.jacoco:org.jacoco.ant [0.7.4.201502262128 -> 0.7.9]
#  - com.android.support.test:runner [1.0.0 -> 1.0.1]

Measure Performance

inline play


TinyDancer

https://github.com/friendlyrobotnyc/TinyDancer

class DebugApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        TinyDancer.create().show(this)
  }
}

TinyDancer

https://github.com/friendlyrobotnyc/TinyDancer

//alternatively
TinyDancer.create()
    .redFlagPercentage(.1f) // set red indicator for 10%....different from default
    .startingXPosition(200)
    .startingYPosition(600)
    .show(this)
// Or
TinyDancer.create()
    .addFrameDataCallback { previousFrameNS, currentFrameNS, droppedFrames ->
        //collect your stats here
    }
    .show(this)

Android Dev Metrics

App slow as they age

The problem with performance is that it often decreases slowly so in day-by-day development it's hard to notice that our app (or Activity or any other view) launches 50ms longer. And another 150ms longer, and another 100ms


Android Dev Metrics

https://github.com/frogermcs/AndroidDevMetrics

buildscript { // Root build.gradle
    apply from: 'dependencies.gradle'
    // Rest of plugins
    classpath gradlePlugins.androidDevMetrics
}
// App build.gradle
apply plugin: 'com.android.application'
apply plugin: 'com.frogermcs.androiddevmetrics'

fitfit


LeakCanary

https://github.com/square/leakcanary

inline


App/build.grade

// Dev tools
dependencies {
    debugImplementation libraries.leakCanary
    releaseImplementation libraries.leakCanaryNoop

    debugImplementation libraries.tinyDancer
    releaseImplementation libraries.tinyDancerNoop
}

LeakCanary

class App : Application() {
var refWatcher: RefWatcher? = null
    private set
    override fun onCreate() {
        super.onCreate()
        if (LeakCanary.isInAnalyzerProcess(this)) { 
            return 
        }
        refWatcher = LeakCanary.install(this)
    }
}

LeakCanary

class App : Application() {
var refWatcher: RefWatcher? = null
    private set
    override fun onCreate() {
        super.onCreate()
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return 
        }
        refWatcher = LeakCanary.install(this)
    }
}

LeakCanary

class App : Application() {
var refWatcher: RefWatcher? = null
    private set
override fun onCreate() {
    super.onCreate()
    if (LeakCanary.isInAnalyzerProcess(this)) {
        return 
    }
    refWatcher = LeakCanary.install(this)
}

LeakCanary

class App : Application() {
    companion object {
        fun getRefWatcher(context: Activity) =
            ((context.applicationContext) as App).refWatcher
    }
    var refWatcher: RefWatcher? = null
        private set
    override fun onCreate() {
        …
    }
}

Stetho

inlineinline


Stetho

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        Stetho.initializeWithDefaults(this)
        // Rest of onCreate…
    }
}

Stetho

class Network {
     // For OkHttp 3.x
    OkHttpClient.Builder()
        .addNetworkInterceptor(StethoInterceptor())
        .build()
    // For OkHttp 2.x
    OkHttpClient().apply {
        networkInterceptors().add(StethoInterceptor())
    }
}

Stetho

class Network {
     // For OkHttp 3.x
    OkHttpClient.Builder()
        .addNetworkInterceptor(StethoInterceptor())
        .build()
    // For OkHttp 2.x
    OkHttpClient().apply {
        networkInterceptors().add(StethoInterceptor())
    }
}

Timber

public class ExampleApp extends Application {
  @Override public void onCreate() {
    super.onCreate();

    if (BuildConfig.DEBUG) {
      // Prints to log cat
      Timber.plant(new DebugTree());
    } else {
      // Does not print to log cat 
      // Instead sends logs to Crash Reporting Tool
      Timber.plant(new CrashReportingTree());
    }
  }

  /** A tree which logs important information for crash reporting. */
  private static class CrashReportingTree extends Timber.Tree {
    @Override protected void log(int priority, String tag, String message, Throwable t) {
      if (priority == Log.VERBOSE || priority == Log.DEBUG) {
        return;
      }

      FakeCrashLibrary.log(priority, tag, message);

      if (t != null) {
        if (priority == Log.ERROR) {
          FakeCrashLibrary.logError(t);
        } else if (priority == Log.WARN) {
          FakeCrashLibrary.logWarning(t);
        }
      }
    }
  }
}

Log Cat in the App

// Application onCreate
val lynxShakeDetector 
    = LynxShakeDetector(this)
    
lynxShakeDetector.init()

right fit 85% play


Fat Application

class App : Application() {
    companion object {
        fun getRefWatcher(context: Activity) =
            ((context.applicationContext) as App).refWatcher
    }
    var refWatcher: RefWatcher? = null
        private set
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            Stetho.initializeWithDefaults(this)
            if (LeakCanary.isInAnalyzerProcess(this)) {
                return 
            }
            refWatcher = LeakCanary.install(this)
            TinyDancer.create()
                .redFlagPercentage(.1f) // set red indicator for 10%....different from default
                .startingXPosition(200)
                .startingYPosition(600)
                .show(this)
         }
    }
}

Trimming the Application Fat

interface Bootstrap {
    fun init()
}
class TinyDancerBootstrap(private val app: Application) : Bootstrap {
    override fun init() { 
        TinyDancer.create().show(app)
    }
}
class DevMetrics(private val app: Application) : Bootstrap {
    override fun init() { 
        AndroidDevMetrics.initWith(app)
    }
}

Trimming the Application Fat

@Module
object BootstrapModule {
    @Provides @Singleton @JvmStatic
    @IntoSet 
    fun provideTinyDancer(app: Application): Bootstrap
        = TinyDancerBootstrap(app)

    @Provides @Singleton @JvmStatic
    @IntoSet 
    fun provideDevMetrics(app: Application): Bootstrap
        = DevMetrics(app)
}

Trimming the Application Fat

@Module
object BootstrapModule {
    @Provides @Singleton @JvmStatic
    @IntoSet 
    fun provideTinyDancer(app: Application): Bootstrap
        = TinyDancerBootstrap(app)

    @Provides @Singleton @JvmStatic
    @IntoSet 
    fun provideDevMetrics(app: Application): Bootstrap
        = DevMetrics(app)
}

Trimming the Application Fat

class App : Application() {
    @Inject
    lateinit var bootstraps: MutableSet<Bootstrap>

    override fun onCreate() {
        super.onCreate()
        DaggerComponent … inject(this)
        if(BuildConfig.DEBUG) {}
            bootstraps.forEach { it.init() }
        }
    }
}

Old Plugins

  • Gradle Retrolambda Plugin Android Studio 3 come with Java 8 support

Links


Moneytree is hiring


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment